From f94560700ed2d755e5261addf8c0dad039f2bd3f Mon Sep 17 00:00:00 2001 From: zml Date: Wed, 1 Feb 2023 17:29:00 -0800 Subject: [PATCH] api: Introduce KSP variant of the annotation processor This should improve the experience for Kotlin users --- api/build.gradle.kts | 66 ++++- .../plugin/ap/PluginAnnotationProcessor.java | 249 ++++++++++++++---- .../ap/PluginProcessingEnvironment.java | 165 ++++++++++++ .../ap/SerializedPluginDescription.java | 38 ++- .../api/plugin/ap/PluginKspProcessor.kt | 183 +++++++++++++ ...ols.ksp.processing.SymbolProcessorProvider | 1 + .../main/kotlin/velocity-spotless.gradle.kts | 17 ++ gradle.properties | 3 + gradle/libs.versions.toml | 2 + 9 files changed, 656 insertions(+), 68 deletions(-) create mode 100644 api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginProcessingEnvironment.java create mode 100644 api/src/ap/kotlin/com/velocitypowered/api/plugin/ap/PluginKspProcessor.kt create mode 100644 api/src/ap/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 4ff26ce52c..90a0a0f1a2 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -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") @@ -39,10 +79,13 @@ 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 { @@ -50,10 +93,9 @@ tasks { manifest { attributes["Automatic-Module-Name"] = "com.velocitypowered.api" } + from(ap.output) } withType { - exclude("com/velocitypowered/api/plugin/ap/**") - val o = options as StandardJavadocDocletOptions o.encoding = "UTF-8" o.source = "8" diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java index c1788da5cc..3e69c23f0c 100644 --- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java @@ -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 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 @@ -54,52 +63,200 @@ public synchronized boolean process(Set 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 { + 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 explicitValues; + private final Map defaultValues; + + JavaAnnotationWrapper(final Elements elements, final AnnotationMirror annotation) { + final Map 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 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 boxedList = (List) boxed; + final List 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 unboxAndCast(final Object o, final Class 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 @Nullable T get(final String key, final Class 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 List getList(final String key, final Class 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 boxedList = (List) value; + final List 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); + } + } } } diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginProcessingEnvironment.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginProcessingEnvironment.java new file mode 100644 index 0000000000..33df1da0be --- /dev/null +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginProcessingEnvironment.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.plugin.ap; + +import com.google.gson.Gson; +import com.sun.source.util.Plugin; +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Shared logic between different processing platforms. + * + * @param the native element type + */ +abstract class PluginProcessingEnvironment { + static final String PLUGIN_CLASS_NAME = "com.velocitypowered.api.plugin.Plugin"; + static final String META_FILE_LOCATION = "velocity-plugin.json"; + + // State + private String pluginClassFound; + private boolean warnedAboutMultiplePlugins; + + // Implementation-specific hooks + + abstract void logNotice(final String message, final E element); + + abstract void logWarning(final String message, final E element); + + abstract void logError(final String message, final E element); + + abstract void logError(final String message, final E element, final Exception ex); + + abstract boolean isClass(final E element); + + abstract String getQualifiedName(final E element); + + abstract AnnotationWrapper getPluginAnnotation(final E element); + + abstract BufferedWriter openWriter( + final String pkg, final String name, final E sourceElement) throws IOException; + + // Shared processing logic + + /** + * Process a single element annotated with the + * {@code com.velocitypowered.api.plugin.Plugin} annotation. + * + * @param element the element + * @return whether further elements should be processed + */ + boolean process(final E element) { + if (!this.preValidate(element)) { + return false; + } + + final String qualifiedName = this.getQualifiedName(element); + if (this.generate(element, qualifiedName)) { + this.pluginClassFound = qualifiedName; + return true; + } + + return false; + } + + // Validation before parsing full annotation + private boolean preValidate(final E element) { + if (!this.isClass(element)) { + this.logError("Only classes can be annotated with " + + Plugin.class.getCanonicalName(), element); + return false; + } + + final String qualifiedName = this.getQualifiedName(element); + + if (Objects.equals(this.pluginClassFound, qualifiedName)) { + if (!this.warnedAboutMultiplePlugins) { + this.logWarning("Velocity does not yet currently support " + + "multiple plugins. We are using " + this.pluginClassFound + + " for your plugin's main class.", element); + this.warnedAboutMultiplePlugins = true; + } + return false; + } + + return true; + } + + + // Validation and generation after fetching full annotation + private boolean generate(final E element, final String qualifiedName) { + final AnnotationWrapper plugin = this.getPluginAnnotation(element); + final String id = plugin.get(SerializedPluginDescription.PLUGIN_ID, String.class); + if (id == null || !SerializedPluginDescription.ID_PATTERN.matcher(id).matches()) { + this.logError("Invalid ID '" + id + "'for plugin " + + ". IDs must start alphabetically, have lowercase alphanumeric characters, and " + + "can contain dashes or underscores.", element); + return false; + } + + // All good, generate the velocity-plugin.json. + final SerializedPluginDescription description = SerializedPluginDescription + .from(plugin, qualifiedName); + try (final var writer = this.openWriter("", META_FILE_LOCATION, element)) { + new Gson().toJson(description, writer); + } catch (final IOException e) { + this.logError("Unable to generate plugin file", element, e); + } + + return true; + } + + /** + * A wrapper around platform annotation types. + * + *

Values are modeled similar to the jx.processing api, with several simplifications

+ *
    + *
  • Primitives as their wrappers
  • + *
  • Strings as Strings
  • + *
  • Classes as their qualified name (a String)
  • + *
  • + *
  • Lists/arrays have a special getter method (though they are exposed as lists)
  • + *
  • Sub-annotations are exposed as annotation wrappers
  • + *
+ * + *

Unboxing of values may be done lazily, as desired by the implementation

+ */ + interface AnnotationWrapper { + /** + * Get a value of a certain type. + * + * @param key the key + * @param expectedType the expected unboxed types + * @return a value, if any is present + * @param the value type + */ + @Nullable T get(final String key, final Class expectedType); + + /** + * Get a value of a certain type. + * + * @param key the key + * @param expectedType the expected unboxed types + * @return a list of values + * @param the value type + */ + @Nullable List getList(final String key, final Class expectedType); + + /** + * Get if a value has been provided explicitly, rather than through a default. + * + * @param key the annotation field + * @return whether this value is explicit + */ + boolean isExplicit(final String key); + } + +} diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java index da0a1ae4bf..f56ebfe9ed 100644 --- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/SerializedPluginDescription.java @@ -10,9 +10,7 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; -import com.velocitypowered.api.plugin.Plugin; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.regex.Pattern; @@ -20,12 +18,21 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** - * Serialized version of {@link com.velocitypowered.api.plugin.PluginDescription}. + * Serialized version of {@code com.velocitypowered.api.plugin.PluginDescription}. */ public final class SerializedPluginDescription { - public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}"); + static final String PLUGIN_ID = "id"; + static final String PLUGIN_NAME = "name"; + static final String PLUGIN_VERSION = "version"; + static final String PLUGIN_DESCRIPTION = "description"; + static final String PLUGIN_URL = "url"; + static final String PLUGIN_AUTHORS = "authors"; + static final String PLUGIN_DEPENDENCIES = "dependencies"; + static final String DEPENDENCY_ID = "id"; + static final String DEPENDENCY_OPTIONAL = "optional"; + // @Nullable is used here to make GSON skip these in the serialized file private final String id; private final @Nullable String name; @@ -52,14 +59,25 @@ private SerializedPluginDescription(String id, String name, String version, Stri this.main = Preconditions.checkNotNull(main, "main"); } - static SerializedPluginDescription from(Plugin plugin, String qualifiedName) { + static SerializedPluginDescription from( + PluginProcessingEnvironment.AnnotationWrapper plugin, + String qualifiedName + ) { List dependencies = new ArrayList<>(); - for (com.velocitypowered.api.plugin.Dependency dependency : plugin.dependencies()) { - dependencies.add(new Dependency(dependency.id(), dependency.optional())); + for (final PluginProcessingEnvironment.AnnotationWrapper dependency : + plugin.getList(PLUGIN_DEPENDENCIES, PluginProcessingEnvironment.AnnotationWrapper.class)) { + dependencies.add(new Dependency( + dependency.get(DEPENDENCY_ID, String.class), + dependency.get(DEPENDENCY_OPTIONAL, Boolean.class) + )); } - return new SerializedPluginDescription(plugin.id(), plugin.name(), plugin.version(), - plugin.description(), plugin.url(), - Arrays.stream(plugin.authors()).filter(author -> !author.isEmpty()) + return new SerializedPluginDescription( + plugin.get(PLUGIN_ID, String.class), + plugin.get(PLUGIN_NAME, String.class), + plugin.get(PLUGIN_VERSION, String.class), + plugin.get(PLUGIN_DESCRIPTION, String.class), + plugin.get(PLUGIN_URL, String.class), + plugin.getList(PLUGIN_AUTHORS, String.class).stream().filter(author -> !author.isEmpty()) .collect(Collectors.toList()), dependencies, qualifiedName); } diff --git a/api/src/ap/kotlin/com/velocitypowered/api/plugin/ap/PluginKspProcessor.kt b/api/src/ap/kotlin/com/velocitypowered/api/plugin/ap/PluginKspProcessor.kt new file mode 100644 index 0000000000..9e7c2ca110 --- /dev/null +++ b/api/src/ap/kotlin/com/velocitypowered/api/plugin/ap/PluginKspProcessor.kt @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.plugin.ap + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.KSPLogger +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.validate +import com.velocitypowered.api.plugin.ap.PluginProcessingEnvironment.AnnotationWrapper +import java.io.BufferedWriter +import java.io.IOException +import java.io.OutputStreamWriter + +class PluginKspProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { + private val pluginEnv: PluginProcessingEnvironment = + KspPluginEnvironment(environment.logger, environment.codeGenerator) + + override fun process(resolver: Resolver): List { + val symbols = resolver.getSymbolsWithAnnotation(PluginProcessingEnvironment.PLUGIN_CLASS_NAME) + // only generate a plugin meta file once all symbols are resolved + // this way any potential generated constants used in the plugin annotation are available + val remaining = symbols.filter { !it.validate() } + + for (symbol in symbols.filter { it.validate() }) { + if (!pluginEnv.process(symbol)) { + break + } + } + + return remaining.toList() + } + + class Provider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return PluginKspProcessor(environment) + } + } + + internal class KspPluginEnvironment( + private val logger: KSPLogger, + private val filer: CodeGenerator + ) : PluginProcessingEnvironment() { + override fun logNotice(message: String, element: KSAnnotated) { + logger.logging(message, element) + } + + override fun logWarning(message: String, element: KSAnnotated) { + logger.warn(message, element) + } + + override fun logError(message: String, element: KSAnnotated) { + logger.error(message, element) + } + + override fun logError(message: String, element: KSAnnotated, ex: Exception?) { + logger.error(message, element) + if (ex != null) { + logger.exception(ex) + } + } + + override fun isClass(element: KSAnnotated?): Boolean { + return element is KSClassDeclaration + } + + override fun getQualifiedName(element: KSAnnotated): String { + return ((element as KSClassDeclaration).qualifiedName ?: element.simpleName).asString() + } + + override fun getPluginAnnotation(element: KSAnnotated): AnnotationWrapper { + val annotation = + element.annotations.find { + it.shortName.asString() == "Plugin" && + it.annotationType.resolve().declaration.qualifiedName!!.asString() == + PLUGIN_CLASS_NAME + } + ?: throw IllegalStateException( + "The provided element $element does not have a @Plugin annotation" + ) + + return KspAnnotationWrapper(annotation) + } + + override fun openWriter(pkg: String, name: String, sourceElement: KSAnnotated): BufferedWriter { + val file = + if (sourceElement is KSDeclaration) { + sourceElement.containingFile + } else { + null + } + + val extIdx = name.lastIndexOf('.') + if (extIdx == -1) { + throw IOException("File name $name did not have an extension!") + } + val fileName = name.substring(0, extIdx) + val extension = name.substring(extIdx + 1, name.length) + + val output = + filer.createNewFile( + Dependencies(aggregating = false, sources = file?.let { arrayOf(it) } ?: arrayOf()), + pkg, + fileName, + extension + ) + return BufferedWriter(OutputStreamWriter(output)) + } + } + + internal class KspAnnotationWrapper(anno: KSAnnotation) : AnnotationWrapper { + private val explicit: Map + private val default: Map + + init { + val explicit = anno.arguments.associate { it.name!!.asString() to it.value } + this.explicit = explicit + default = + anno.defaultArguments + .filter { it.name!!.asString() !in explicit } + .associate { it.name!!.asString() to it.value } + } + + private fun unbox(arg: Any?): Any? { + return when (arg) { + is KSTypeReference -> arg // TODO: not used for Velocity + is KSPropertyDeclaration -> arg.simpleName.asString() + is KSAnnotation -> KspAnnotationWrapper(arg) + is List<*> -> { + val unboxed = mutableListOf() + for (o in arg) { + unboxed.add(this.unbox(o)!!) + } + unboxed + } + else -> arg + } + } + + private fun unboxAndCast(o: Any, expectedType: Class): T { + val value = unbox(o) + require(expectedType.isInstance(value)) { + ("Expected '$value' to be a $expectedType, but it was not") + } + + return expectedType.cast(value) + } + + override fun get(key: String, expectedType: Class): T? { + val value: Any = this.explicit[key] ?: this.default[key] ?: return null + + return unboxAndCast(value, expectedType) + } + + override fun getList(key: String?, expectedType: Class): List? { + val value: Any = this.explicit[key] ?: this.default[key] ?: return null + + return if (value is List<*>) { + value.map { unboxAndCast(it!!, expectedType) } + } else { + listOf(unboxAndCast(value, expectedType)) + } + } + + override fun isExplicit(key: String?): Boolean { + return key in explicit + } + } +} diff --git a/api/src/ap/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/api/src/ap/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000000..d7ae800dc9 --- /dev/null +++ b/api/src/ap/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +com.velocitypowered.api.plugin.ap.PluginKspProcessor$Provider \ No newline at end of file diff --git a/build-logic/src/main/kotlin/velocity-spotless.gradle.kts b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts index 59422591c3..9276c46ce6 100644 --- a/build-logic/src/main/kotlin/velocity-spotless.gradle.kts +++ b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts @@ -13,4 +13,21 @@ extensions.configure { } removeUnusedImports() } + plugins.withId("org.jetbrains.kotlin.jvm") { + kotlin { + if (project.name == "velocity-api") { + licenseHeaderFile(project.file("HEADER.txt")) + } else { + licenseHeaderFile(project.rootProject.file("HEADER.txt")) + } + ktfmt() + .kotlinlangStyle() + .configure { + it.setMaxWidth(100) + it.setBlockIndent(2) + it.setContinuationIndent(4) + it.setRemoveUnusedImport(true) + } + } + } } diff --git a/gradle.properties b/gradle.properties index cd9c67796f..38e73adaf8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,5 @@ group=com.velocitypowered version=3.3.0-SNAPSHOT + +# Kotlin needs this... +kotlin.stdlib.default.dependency=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f0e89ed57d..cde95f9d3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ configurate3 = "3.7.3" configurate4 = "4.1.2" flare = "2.0.1" +ksp = "1.8.0-1.0.9" log4j = "2.20.0" netty = "4.1.100.Final" @@ -34,6 +35,7 @@ jline = "org.jline:jline-terminal-jansi:3.23.0" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" junit = "org.junit.jupiter:junit-jupiter:5.9.0" jspecify = "org.jspecify:jspecify:0.3.0" +kspApi = "com.google.devtools.ksp:symbol-processing-api:1.9.22-1.0.17" kyori-ansi = "net.kyori:ansi:1.0.3" guava = "com.google.guava:guava:25.1-jre" gson = "com.google.code.gson:gson:2.10.1"