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

Add support for descriptions of record components in configuration metadata generation #29403

Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ include::{docs-java}/features/developingautoconfiguration/customstarter/configur

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".
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 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.
Expand All @@ -23,6 +23,7 @@
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;
Expand All @@ -34,13 +35,17 @@
* 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
Expand All @@ -59,6 +64,15 @@ protected Object resolveDefaultValue(MetadataGenerationEnvironment environment)
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 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.
Expand Down Expand Up @@ -155,7 +155,7 @@ private String resolveType(MetadataGenerationEnvironment environment) {
return environment.getTypeUtils().getType(getOwnerElement(), getType());
}

private String resolveDescription(MetadataGenerationEnvironment environment) {
protected String resolveDescription(MetadataGenerationEnvironment environment) {
return environment.getTypeUtils().getJavaDoc(getField());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
Expand All @@ -25,6 +25,7 @@
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.ExecutableElement;
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;
Expand All @@ -35,6 +36,7 @@
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Pavel Anisimov
*/
class PropertyDescriptorResolver {

Expand Down Expand Up @@ -82,8 +84,9 @@ Stream<PropertyDescriptor<?>> resolveConstructorProperties(TypeElement type, Exe
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, factoryMethod, parameter, name,
propertyType, field, getter, setter));
propertyType, field, recordComponent, getter, setter));
});
return candidates.values().stream();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
Expand Down Expand Up @@ -27,6 +27,7 @@
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;
Expand All @@ -38,6 +39,7 @@
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Pavel Anisimov
*/
class TypeElementMembers {

Expand All @@ -49,6 +51,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<>();
Expand All @@ -66,6 +70,9 @@ private void process(TypeElement element) {
for (VariableElement field : ElementFilter.fieldsIn(element.getEnclosedElements())) {
processField(field);
}
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())) {
process((TypeElement) superType);
Expand Down Expand Up @@ -163,10 +170,21 @@ private void processField(VariableElement 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
Expand All @@ -24,11 +24,13 @@
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;
Expand All @@ -44,6 +46,7 @@
*
* @author Stephane Nicoll
* @author Phillip Webb
* @author Pavel Anisimov
*/
class TypeUtils {

Expand Down Expand Up @@ -176,6 +179,9 @@ boolean isCollectionOrMap(TypeMirror type) {
}

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();
Expand Down Expand Up @@ -246,6 +252,24 @@ private void process(TypeDescriptor descriptor, TypeMirror type) {
}
}

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
Expand Down Expand Up @@ -74,6 +74,7 @@
* @author Andy Wilkinson
* @author Kris De Volder
* @author Jonas Keßler
* @author Pavel Anisimov
*/
class ConfigurationMetadataAnnotationProcessorTests extends AbstractMetadataGenerationTests {

Expand Down Expand Up @@ -460,4 +461,45 @@ void multiConstructorRecordProperties(@TempDir File temp) throws IOException {
assertThat(metadata).doesNotHave(Metadata.withProperty("multi.some-integer"));
}

@Test
@EnabledForJreRange(min = JRE.JAVA_16)
void recordPropertiesWithDescriptions(@TempDir File temp) throws IOException {
File exampleRecord = new File(temp, "ExampleRecord.java");
try (PrintWriter writer = new PrintWriter(new FileWriter(exampleRecord))) {
writer.println("/**");
writer.println(" * ExampleRecord Javadoc sample");
writer.println(" *");
writer.println(" * @author Pavel Anisimov");
writer.println(" * @param someString very long description that doesn't fit");
writer.println(" * single line");
writer.println(" * @param someInteger description with @param and @ pitfalls");
writer.println(" * @param someBoolean description with extra spaces");
writer.println(" *@param someLong description without space after asterisk");
writer.println(" * @since 1.0.0");
writer.println(" * @param someByte last description in Javadoc");
writer.println(" */");
writer.println(
"@org.springframework.boot.configurationsample.ConfigurationProperties(\"record.descriptions\")");
writer.println("public record ExampleRecord(");
writer.println("String someString,");
writer.println("Integer someInteger,");
writer.println("Boolean someBoolean,");
writer.println("Long someLong,");
writer.println("Byte someByte");
writer.println(") {");
writer.println("}");
}
ConfigurationMetadata metadata = compile(exampleRecord);
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"));
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 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.
Expand Down Expand Up @@ -126,7 +126,7 @@ void constructorParameterDeprecatedPropertyOnGetter() throws IOException {
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();
});
}
Expand Down Expand Up @@ -213,7 +213,7 @@ protected ConstructorParameterPropertyDescriptor createPropertyDescriptor(TypeEl
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) {
Expand Down