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

Interface-based config support #412

Draft
wants to merge 36 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f24998d
Initial work to support interface-based configs
Tim203 Jul 2, 2023
efcb213
Allow all tests to run properly
Tim203 Jul 3, 2023
b29d241
Split some classes and added Range annotations
Tim203 Jan 3, 2024
e4b7f7f
Added default value annotations and restructured tests
Tim203 Jan 3, 2024
948cebe
Allow setter return type to be non-void
Tim203 Jan 3, 2024
ba11369
Auto-generate simple mappings
Tim203 Jan 3, 2024
7f6059f
Added Hidden annotation and added Processor.AdvancedFactory to aid it
Tim203 Jan 5, 2024
78bcaf5
Added support for some build-in annotations and added another addProc…
Tim203 Jan 6, 2024
c8524c8
Started working on adding tests for interfaces runtime
Tim203 Jan 6, 2024
336de1c
Merge remote-tracking branch 'origin/master' into feature/interfaces
Tim203 Jan 6, 2024
6f23846
Use correct impl name for mappings
Tim203 Jan 6, 2024
f07dc2f
chore(build): Only set test flags on newer JDK versions
zml2008 Jan 7, 2024
6c7f27c
Add all annotations that support fields. Use messager for errors
Tim203 Jan 13, 2024
c009449
Made AnnotationDefaults easier to follow
Tim203 Jan 13, 2024
a9c0e2f
Added support for default getters and default setters
Tim203 Jan 14, 2024
e0d9d42
Notify users about Hidden limitation. Optimized Hidden constraint
Tim203 Jan 14, 2024
224e87c
Exit gracefully on failure
Tim203 Jan 14, 2024
b62cab5
Add support for Gradle incremental annotation processing
Tim203 Feb 1, 2024
2d89b9d
Added Field annotation
Tim203 Feb 11, 2024
40d1c07
Apply spotless
Tim203 Feb 11, 2024
ab13924
Applied forbiddenApi fixes
Tim203 Feb 11, 2024
c5533d5
Renamed error to printError to trick PMD
Tim203 Feb 11, 2024
e9c0dfc
spotlessApply
Tim203 Feb 11, 2024
ebed0c5
Fix pmdTest
Tim203 Feb 11, 2024
27786f0
Set core as api dependency
Tim203 Feb 12, 2024
2add4c9
Use superinterface instead of enclosed element
Tim203 Feb 12, 2024
c2dfc96
Set a default value for config sections
Tim203 Feb 12, 2024
1af436d
Update test
Tim203 Feb 12, 2024
7e45e31
Added serialization to InterfaceTypeSerializer
Tim203 Feb 13, 2024
6945a5d
Respect superclasses' declaration of Exclude
Camotoy May 26, 2024
edd0685
Friendly error if implementation name cannot be found
Camotoy May 26, 2024
4347700
Made it easier to use the interface's default options
Tim203 Jun 12, 2024
e79e4d8
Oops, it's the other way around woo
Tim203 Jun 12, 2024
31c63fd
Superclasses with ConfigSerializable define order
Camotoy Aug 21, 2024
7543cd4
Don't try to initialize ConfigSerializable if @Field is marked
Camotoy Aug 24, 2024
7b4769c
InterfaceDefaultOptions#addTo with ObjectMapper modification
Camotoy Sep 10, 2024
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
24 changes: 24 additions & 0 deletions extra/interface/ap/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
plugins {
id "org.spongepowered.configurate.build.component"
}

description = "Annotation processor for Configurate to generate an implementation for config interfaces"

dependencies {
implementation projects.core
implementation projects.extra.extraInterface
implementation libs.javapoet
implementation libs.auto.service

testImplementation libs.compile.testing
}

// there is no javadoc
tasks.withType(Javadoc).configureEach { enabled = false }

tasks.withType(Test).configureEach {
// See: https://github.com/google/compile-testing/issues/222
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locally you just test with whatever your current JVM runtime is -- this fails on CI because we execute tests on a bunch of different JVMs, including J8 where these module args don't exist. You can simulate this conditional locally with the -PstrictMultireleaseVersions=true gradle property (or just setting the CI environment variable).

jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED'
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.spongepowered.configurate.interfaces.processor;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import org.spongepowered.configurate.interfaces.Constants;
import org.spongepowered.configurate.interfaces.meta.Exclude;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;

import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
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.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.AnnotatedConstruct;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

@AutoService(Processor.class)
class ConfigImplementationGeneratorProcessor extends AbstractProcessor {

private final Properties mappings = new Properties();
private Types typeUtils;
private Filer filer;
private Messager messager;

@Override
@SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel")
public synchronized void init(final ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.typeUtils = processingEnv.getTypeUtils();
this.filer = processingEnv.getFiler();
this.messager = processingEnv.getMessager();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return Collections.singleton(ConfigSerializable.class.getCanonicalName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally we can do this without putting configurate itself on the processor path maybe?

}

@Override
public boolean process(final Set<? extends TypeElement> ignored, final RoundEnvironment env) {
if (env.processingOver()) {
if (!env.errorRaised()) {
writeMappings();
}
return false;
}

for (final Element element : env.getElementsAnnotatedWith(ConfigSerializable.class)) {
if (element.getKind() != ElementKind.INTERFACE) {
continue;
}
final TypeElement typeElement = (TypeElement) element;

// nested classes are handled in their containing interfaces
if (isNestedConfig(typeElement)) {
continue;
}

try {
processInterface(typeElement, this.mappings);
} catch (final IOException exception) {
throw new RuntimeException(exception);
}
}

return false;
}

/**
* Generate a class for the given interface and
* returns the name of the generated class.
*/
private void processInterface(final TypeElement type, final Properties generatedClasses) throws IOException {
final ClassName className = ClassName.get(type);
final TypeSpec spec = generateImplementation(type, generatedClasses).build();

JavaFile.builder(className.packageName(), spec)
.build()
.writeTo(this.filer);
}

private TypeSpec.Builder generateImplementation(final TypeElement type, final Properties generatedClasses) {
final ClassName className = ClassName.get(type);

info("Generating implementation for %s", type);

final TypeSpec.Builder spec = TypeSpec
.classBuilder(className.simpleName() + "Impl")
.addSuperinterface(className)
.addModifiers(Modifier.FINAL)
.addAnnotation(ConfigSerializable.class)
.addJavadoc("Automatically generated implementation of the config");

final TypeSpecBuilderTracker tracker = new TypeSpecBuilderTracker();
gatherElementSpec(tracker, type, generatedClasses);
tracker.writeTo(spec);

final String qualifiedName = className.reflectionName();
generatedClasses.put(qualifiedName, qualifiedName + "Impl");
info("Generated implementation for %s", type);

return spec;
}

private void gatherElementSpec(
final TypeSpecBuilderTracker spec,
final TypeElement type,
final Properties generatedClasses
) {
// first handle own elements

for (final Element enclosedElement : type.getEnclosedElements()) {
final ElementKind kind = enclosedElement.getKind();

if (kind == ElementKind.INTERFACE && hasAnnotation(enclosedElement, ConfigSerializable.class)) {
spec.add(
enclosedElement.getSimpleName().toString(),
generateImplementation((TypeElement) enclosedElement, generatedClasses)
.addModifiers(Modifier.STATIC)
);
continue;
}
if (kind != ElementKind.METHOD) {
continue;
}

final ExecutableElement element = (ExecutableElement) enclosedElement;

final boolean excluded = hasAnnotation(element, Exclude.class);
if (element.isDefault()) {
if (excluded) {
// no need to handle them
continue;
}
info("Overriding implementation for %s as it's not excluded", element);
} else if (excluded) {
throw new IllegalStateException(String.format(
Locale.ROOT,
"Cannot make config due to method %s, which is an excluded method that has no implementation!",
element
));
}

final List<? extends VariableElement> parameters = element.getParameters();
if (parameters.size() > 1) {
throw new IllegalStateException("Setters cannot have more than one parameter! Method: " + element);
}

final String simpleName = element.getSimpleName().toString();
TypeMirror nodeType = element.getReturnType();

if (parameters.size() == 1) {
final VariableElement parameter = parameters.get(0);
// setter
spec.add(
simpleName + "#" + parameter.getSimpleName().toString(),
MethodSpec.overriding(element)
.addStatement(
"this.$N = $N",
element.getSimpleName(),
parameter.getSimpleName()
)
);
nodeType = parameter.asType();
} else {
// getter
spec.add(
simpleName,
MethodSpec.overriding(element)
.addStatement("return $N", element.getSimpleName())
);
}

spec.add(simpleName, FieldSpec.builder(TypeName.get(nodeType), simpleName, Modifier.PRIVATE));
}

// then handle parent elements
for (final TypeMirror parent : type.getInterfaces()) {
gatherElementSpec(spec, (TypeElement) this.typeUtils.asElement(parent), generatedClasses);
}
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

private void writeMappings() {
final FileObject resource;
try {
resource = this.filer.createResource(StandardLocation.SOURCE_OUTPUT, "", Constants.MAPPING_FILE);
try (Writer writer = resource.openWriter()) {
this.mappings.store(writer, null);
}
} catch (final IOException exception) {
throw new RuntimeException("Failed to write interface mappings!", exception);
}
}

private boolean hasAnnotation(final AnnotatedConstruct element, final Class<? extends Annotation> annotation) {
//noinspection ConstantValue not everything is nonnull by default
return element.getAnnotation(annotation) != null;
}

private boolean isNestedConfig(final TypeElement type) {
if (!type.getNestingKind().isNested()) {
return false;
}

Element current = type;
while (current.getKind() == ElementKind.INTERFACE && hasAnnotation(current, ConfigSerializable.class)) {
current = current.getEnclosingElement();
}
return current.getKind() == ElementKind.PACKAGE;
}

private void info(final String message, final Object... arguments) {
this.messager.printMessage(Kind.NOTE, String.format(Locale.ROOT, message, arguments));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Configurate
* Copyright (C) zml and Configurate contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.spongepowered.configurate.interfaces.processor;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* {@link TypeSpec.Builder} does not keep track of duplicates, resulting in failures to compile.
* This will only allow a single definition of a given method/field
*/
class TypeSpecBuilderTracker {

private final Map<String, FieldSpec.Builder> fieldSpecs = new LinkedHashMap<>();
private final Map<String, MethodSpec.Builder> methodSpecs = new LinkedHashMap<>();
private final Map<String, TypeSpec> typeSpecs = new LinkedHashMap<>();

void add(final String fieldIdentifier, final FieldSpec.Builder builder) {
final FieldSpec.Builder existing = this.fieldSpecs.get(fieldIdentifier);
if (existing != null) {
existing.addAnnotations(originalAnnotations(existing.build().annotations, builder.build().annotations));
return;
}
this.fieldSpecs.put(fieldIdentifier, builder);
}

void add(final String methodIdentifier, final MethodSpec.Builder builder) {
final MethodSpec.Builder existing = this.methodSpecs.get(methodIdentifier);
if (existing != null) {
existing.addAnnotations(originalAnnotations(existing.build().annotations, builder.build().annotations));
return;
}
this.methodSpecs.put(methodIdentifier, builder);
}

void add(final String typeIdentifier, final TypeSpec.Builder builder) {
if (this.typeSpecs.putIfAbsent(typeIdentifier, builder.build()) != null) {
throw new IllegalStateException(
"Cannot have multiple nested types with the same name! Name: " + typeIdentifier);
}
}

void writeTo(final TypeSpec.Builder builder) {
for (FieldSpec.Builder field : this.fieldSpecs.values()) {
builder.addField(field.build());
}
for (MethodSpec.Builder method : this.methodSpecs.values()) {
builder.addMethod(method.build());
}
this.typeSpecs.values().forEach(builder::addType);
}

private List<AnnotationSpec> originalAnnotations(
final List<AnnotationSpec> left,
final List<AnnotationSpec> right
) {
final List<AnnotationSpec> result = new ArrayList<>(left);
result.removeAll(right);
return result;
}

}
Loading