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:
Pavel Anisimov
2022-01-14 18:26:46 +03:00
committed by Phillip Webb
parent cde9166d50
commit af976caec9
10 changed files with 117 additions and 6 deletions

View File

@@ -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".

View File

@@ -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.

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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"));
}
}

View File

@@ -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) {

View File

@@ -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) {
}