Generate configuration metadata for records
Update `spring-boot-configuration-processor` to support generating configuration metadata from record parameter javadoc. See gh-29403
This commit is contained in:
committed by
Phillip Webb
parent
cde9166d50
commit
af976caec9
@@ -255,6 +255,8 @@ include-code::AcmeProperties[]
|
||||
|
||||
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
|
||||
|
||||
If you use `@ConfigurationProperties` with record class then record components' descriptions should be provided via class-level Javadoc tag `@param` (there are no explicit instance fields in record classes to put regular field-level Javadocs on).
|
||||
|
||||
Here are some rules we follow internally to make sure descriptions are consistent:
|
||||
|
||||
* Do not start the description by "The" or "A".
|
||||
|
||||
@@ -89,6 +89,8 @@ The Javadoc on fields is used to populate the `description` attribute. For insta
|
||||
|
||||
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
|
||||
|
||||
If you use `@ConfigurationProperties` with record class then record components' descriptions should be provided via class-level Javadoc tag `@param` (there are no explicit instance fields in record classes to put regular field-level Javadocs on).
|
||||
|
||||
The annotation processor applies a number of heuristics to extract the default value from the source model.
|
||||
Default values have to be provided statically. In particular, do not refer to a constant defined in another class.
|
||||
Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s.
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.function.Function;
|
||||
import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.PrimitiveType;
|
||||
@@ -34,13 +35,17 @@ import javax.tools.Diagnostic.Kind;
|
||||
* A {@link PropertyDescriptor} for a constructor parameter.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Pavel Anisimov
|
||||
*/
|
||||
class ConstructorParameterPropertyDescriptor extends PropertyDescriptor<VariableElement> {
|
||||
|
||||
private final RecordComponentElement recordComponent;
|
||||
|
||||
ConstructorParameterPropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod,
|
||||
VariableElement source, String name, TypeMirror type, VariableElement field, ExecutableElement getter,
|
||||
ExecutableElement setter) {
|
||||
VariableElement source, String name, TypeMirror type, VariableElement field,
|
||||
RecordComponentElement recordComponent, ExecutableElement getter, ExecutableElement setter) {
|
||||
super(ownerElement, factoryMethod, source, name, type, field, getter, setter);
|
||||
this.recordComponent = recordComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,6 +64,15 @@ class ConstructorParameterPropertyDescriptor extends PropertyDescriptor<Variable
|
||||
return getSource().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveDescription(MetadataGenerationEnvironment environment) {
|
||||
// record components descriptions are written using @param tag
|
||||
if (this.recordComponent != null) {
|
||||
return environment.getTypeUtils().getJavaDoc(this.recordComponent);
|
||||
}
|
||||
return super.resolveDescription(environment);
|
||||
}
|
||||
|
||||
private Object getDefaultValueFromAnnotation(MetadataGenerationEnvironment environment, Element element) {
|
||||
AnnotationMirror annotation = environment.getDefaultValueAnnotation(element);
|
||||
List<String> defaultValue = getDefaultValue(environment, annotation);
|
||||
|
||||
@@ -155,7 +155,7 @@ abstract class PropertyDescriptor<S extends Element> {
|
||||
return environment.getTypeUtils().getType(getOwnerElement(), getType());
|
||||
}
|
||||
|
||||
private String resolveDescription(MetadataGenerationEnvironment environment) {
|
||||
protected String resolveDescription(MetadataGenerationEnvironment environment) {
|
||||
return environment.getTypeUtils().getJavaDoc(getField());
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import javax.lang.model.element.AnnotationMirror;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.NestingKind;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
@@ -36,6 +37,7 @@ import javax.lang.model.util.ElementFilter;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @author Pavel Anisimov
|
||||
*/
|
||||
class PropertyDescriptorResolver {
|
||||
|
||||
@@ -82,8 +84,9 @@ class PropertyDescriptorResolver {
|
||||
ExecutableElement getter = members.getPublicGetter(name, propertyType);
|
||||
ExecutableElement setter = members.getPublicSetter(name, propertyType);
|
||||
VariableElement field = members.getFields().get(name);
|
||||
RecordComponentElement recordComponent = members.getRecordComponents().get(name);
|
||||
register(candidates, new ConstructorParameterPropertyDescriptor(type, null, parameter, name, propertyType,
|
||||
field, getter, setter));
|
||||
field, recordComponent, getter, setter));
|
||||
});
|
||||
return candidates.values().stream();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.function.Function;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
@@ -39,6 +40,7 @@ import javax.lang.model.util.ElementFilter;
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @author Moritz Halbritter
|
||||
* @author Pavel Anisimov
|
||||
*/
|
||||
class TypeElementMembers {
|
||||
|
||||
@@ -54,6 +56,8 @@ class TypeElementMembers {
|
||||
|
||||
private final Map<String, VariableElement> fields = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, RecordComponentElement> recordComponents = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, List<ExecutableElement>> publicGetters = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, List<ExecutableElement>> publicSetters = new LinkedHashMap<>();
|
||||
@@ -72,6 +76,9 @@ class TypeElementMembers {
|
||||
for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
|
||||
processMethod(method);
|
||||
}
|
||||
for (RecordComponentElement recordComponent : ElementFilter.recordComponentsIn(element.getEnclosedElements())) {
|
||||
processRecordComponent(recordComponent);
|
||||
}
|
||||
Element superType = this.env.getTypeUtils().asElement(element.getSuperclass());
|
||||
if (superType instanceof TypeElement && !OBJECT_CLASS_NAME.equals(superType.toString())
|
||||
&& !RECORD_CLASS_NAME.equals(superType.toString())) {
|
||||
@@ -189,10 +196,21 @@ class TypeElementMembers {
|
||||
this.fields.putIfAbsent(name, field);
|
||||
}
|
||||
|
||||
private void processRecordComponent(RecordComponentElement recordComponent) {
|
||||
String name = recordComponent.getSimpleName().toString();
|
||||
if (!this.recordComponents.containsKey(name)) {
|
||||
this.recordComponents.put(name, recordComponent);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, VariableElement> getFields() {
|
||||
return Collections.unmodifiableMap(this.fields);
|
||||
}
|
||||
|
||||
Map<String, RecordComponentElement> getRecordComponents() {
|
||||
return Collections.unmodifiableMap(this.recordComponents);
|
||||
}
|
||||
|
||||
Map<String, List<ExecutableElement>> getPublicGetters() {
|
||||
return Collections.unmodifiableMap(this.publicGetters);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.RecordComponentElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
@@ -44,6 +46,7 @@ import javax.lang.model.util.Types;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Phillip Webb
|
||||
* @author Pavel Anisimov
|
||||
*/
|
||||
class TypeUtils {
|
||||
|
||||
@@ -176,6 +179,9 @@ class TypeUtils {
|
||||
}
|
||||
|
||||
String getJavaDoc(Element element) {
|
||||
if (element instanceof RecordComponentElement) {
|
||||
return getJavaDoc((RecordComponentElement) element);
|
||||
}
|
||||
String javadoc = (element != null) ? this.env.getElementUtils().getDocComment(element) : null;
|
||||
if (javadoc != null) {
|
||||
javadoc = NEW_LINE_PATTERN.matcher(javadoc).replaceAll("").trim();
|
||||
@@ -247,6 +253,24 @@ class TypeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private String getJavaDoc(RecordComponentElement recordComponent) {
|
||||
String recordJavadoc = this.env.getElementUtils().getDocComment(recordComponent.getEnclosingElement());
|
||||
if (recordJavadoc != null) {
|
||||
Pattern paramJavadocPattern = paramJavadocPattern(recordComponent.getSimpleName().toString());
|
||||
Matcher paramJavadocMatcher = paramJavadocPattern.matcher(recordJavadoc);
|
||||
if (paramJavadocMatcher.find()) {
|
||||
String paramJavadoc = NEW_LINE_PATTERN.matcher(paramJavadocMatcher.group()).replaceAll("").trim();
|
||||
return paramJavadoc.isEmpty() ? null : paramJavadoc;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Pattern paramJavadocPattern(String paramName) {
|
||||
String pattern = String.format("(?<=@param +%s).*?(?=([\r\n]+ *@)|$)", paramName);
|
||||
return Pattern.compile(pattern, Pattern.DOTALL);
|
||||
}
|
||||
|
||||
/**
|
||||
* A visitor that extracts the fully qualified name of a type, including generic
|
||||
* information.
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.configurationprocessor.metadata.ConfigurationMet
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.Metadata;
|
||||
import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration;
|
||||
import org.springframework.boot.configurationsample.record.ExampleRecord;
|
||||
import org.springframework.boot.configurationsample.record.RecordWithGetter;
|
||||
import org.springframework.boot.configurationsample.recursive.RecursiveProperties;
|
||||
import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties;
|
||||
@@ -516,4 +517,19 @@ class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGene
|
||||
assertThat(metadata).has(Metadata.withProperty("spring.datasource.dbcp2.password").withNoDeprecation());
|
||||
}
|
||||
|
||||
@Test
|
||||
void recordPropertiesWithDescriptions() {
|
||||
ConfigurationMetadata metadata = compile(ExampleRecord.class);
|
||||
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-string", String.class)
|
||||
.withDescription("very long description that doesn't fit single line"));
|
||||
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-integer", Integer.class)
|
||||
.withDescription("description with @param and @ pitfalls"));
|
||||
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-boolean", Boolean.class)
|
||||
.withDescription("description with extra spaces"));
|
||||
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-long", Long.class)
|
||||
.withDescription("description without space after asterisk"));
|
||||
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-byte", Byte.class)
|
||||
.withDescription("last description in Javadoc"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest
|
||||
VariableElement field = getField(ownerElement, "flag");
|
||||
VariableElement constructorParameter = getConstructorParameter(ownerElement, "flag");
|
||||
ConstructorParameterPropertyDescriptor property = new ConstructorParameterPropertyDescriptor(ownerElement,
|
||||
null, constructorParameter, "flag", field.asType(), field, getter, null);
|
||||
null, constructorParameter, "flag", field.asType(), field, null, getter, null);
|
||||
assertItemMetadata(metadataEnv, property).isProperty().isDeprecatedWithNoInformation();
|
||||
});
|
||||
}
|
||||
@@ -223,7 +223,7 @@ class ConstructorParameterPropertyDescriptorTests extends PropertyDescriptorTest
|
||||
ExecutableElement getter = getMethod(ownerElement, createAccessorMethodName("get", name));
|
||||
ExecutableElement setter = getMethod(ownerElement, createAccessorMethodName("set", name));
|
||||
return new ConstructorParameterPropertyDescriptor(ownerElement, null, constructorParameter, name,
|
||||
field.asType(), field, getter, setter);
|
||||
field.asType(), field, null, getter, setter);
|
||||
}
|
||||
|
||||
private VariableElement getConstructorParameter(TypeElement ownerElement, String name) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.springframework.boot.configurationsample.record;
|
||||
|
||||
/**
|
||||
* Example Record Javadoc sample
|
||||
*
|
||||
* @param someString very long description that doesn't fit single line
|
||||
* @param someInteger description with @param and @ pitfalls
|
||||
* @param someBoolean description with extra spaces
|
||||
* @param someLong description without space after asterisk
|
||||
* @param someByte last description in Javadoc
|
||||
* @since 1.0.0
|
||||
* @author Pavel Anisimov
|
||||
*/
|
||||
@org.springframework.boot.configurationsample.ConfigurationProperties("record.descriptions")
|
||||
public record ExampleRecord(String someString, Integer someInteger, Boolean someBoolean, Long someLong, Byte someByte) {
|
||||
}
|
||||
Reference in New Issue
Block a user