-
-
Notifications
You must be signed in to change notification settings - Fork 68
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
Tim203
wants to merge
36
commits into
SpongePowered:master
Choose a base branch
from
Tim203:feature/interfaces
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 efcb213
Allow all tests to run properly
Tim203 b29d241
Split some classes and added Range annotations
Tim203 e4b7f7f
Added default value annotations and restructured tests
Tim203 948cebe
Allow setter return type to be non-void
Tim203 ba11369
Auto-generate simple mappings
Tim203 7f6059f
Added Hidden annotation and added Processor.AdvancedFactory to aid it
Tim203 78bcaf5
Added support for some build-in annotations and added another addProc…
Tim203 c8524c8
Started working on adding tests for interfaces runtime
Tim203 336de1c
Merge remote-tracking branch 'origin/master' into feature/interfaces
Tim203 6f23846
Use correct impl name for mappings
Tim203 f07dc2f
chore(build): Only set test flags on newer JDK versions
zml2008 6c7f27c
Add all annotations that support fields. Use messager for errors
Tim203 c009449
Made AnnotationDefaults easier to follow
Tim203 a9c0e2f
Added support for default getters and default setters
Tim203 e0d9d42
Notify users about Hidden limitation. Optimized Hidden constraint
Tim203 224e87c
Exit gracefully on failure
Tim203 b62cab5
Add support for Gradle incremental annotation processing
Tim203 2d89b9d
Added Field annotation
Tim203 40d1c07
Apply spotless
Tim203 ab13924
Applied forbiddenApi fixes
Tim203 c5533d5
Renamed error to printError to trick PMD
Tim203 e9c0dfc
spotlessApply
Tim203 ebed0c5
Fix pmdTest
Tim203 27786f0
Set core as api dependency
Tim203 2add4c9
Use superinterface instead of enclosed element
Tim203 c2dfc96
Set a default value for config sections
Tim203 1af436d
Update test
Tim203 7e45e31
Added serialization to InterfaceTypeSerializer
Tim203 6945a5d
Respect superclasses' declaration of Exclude
Camotoy edd0685
Friendly error if implementation name cannot be found
Camotoy 4347700
Made it easier to use the interface's default options
Tim203 e79e4d8
Oops, it's the other way around woo
Tim203 31c63fd
Superclasses with ConfigSerializable define order
Camotoy 7543cd4
Don't try to initialize ConfigSerializable if @Field is marked
Camotoy 7b4769c
InterfaceDefaultOptions#addTo with ObjectMapper modification
Camotoy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED' | ||
jvmArgs '--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED' | ||
} |
263 changes: 263 additions & 0 deletions
263
...pongepowered/configurate/interfaces/processor/ConfigImplementationGeneratorProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
|
||
} |
83 changes: 83 additions & 0 deletions
83
.../main/java/org/spongepowered/configurate/interfaces/processor/TypeSpecBuilderTracker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).