Skip to content

Commit af976ca

Browse files
An1s9nphilwebb
authored andcommitted
Generate configuration metadata for records
Update `spring-boot-configuration-processor` to support generating configuration metadata from record parameter javadoc. See gh-29403
1 parent cde9166 commit af976ca

File tree

10 files changed

+117
-6
lines changed

10 files changed

+117
-6
lines changed

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/developing-auto-configuration.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ include-code::AcmeProperties[]
255255

256256
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
257257

258+
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).
259+
258260
Here are some rules we follow internally to make sure descriptions are consistent:
259261

260262
* Do not start the description by "The" or "A".

spring-boot-project/spring-boot-docs/src/docs/antora/modules/specification/pages/configuration-metadata/annotation-processor.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ The Javadoc on fields is used to populate the `description` attribute. For insta
8989

9090
NOTE: You should only use plain text with `@ConfigurationProperties` field Javadoc, since they are not processed before being added to the JSON.
9191

92+
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).
93+
9294
The annotation processor applies a number of heuristics to extract the default value from the source model.
9395
Default values have to be provided statically. In particular, do not refer to a constant defined in another class.
9496
Also, the annotation processor cannot auto-detect default values for ``Enum``s and ``Collections``s.

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javax.lang.model.element.AnnotationMirror;
2424
import javax.lang.model.element.Element;
2525
import javax.lang.model.element.ExecutableElement;
26+
import javax.lang.model.element.RecordComponentElement;
2627
import javax.lang.model.element.TypeElement;
2728
import javax.lang.model.element.VariableElement;
2829
import javax.lang.model.type.PrimitiveType;
@@ -34,13 +35,17 @@
3435
* A {@link PropertyDescriptor} for a constructor parameter.
3536
*
3637
* @author Stephane Nicoll
38+
* @author Pavel Anisimov
3739
*/
3840
class ConstructorParameterPropertyDescriptor extends PropertyDescriptor<VariableElement> {
3941

42+
private final RecordComponentElement recordComponent;
43+
4044
ConstructorParameterPropertyDescriptor(TypeElement ownerElement, ExecutableElement factoryMethod,
41-
VariableElement source, String name, TypeMirror type, VariableElement field, ExecutableElement getter,
42-
ExecutableElement setter) {
45+
VariableElement source, String name, TypeMirror type, VariableElement field,
46+
RecordComponentElement recordComponent, ExecutableElement getter, ExecutableElement setter) {
4347
super(ownerElement, factoryMethod, source, name, type, field, getter, setter);
48+
this.recordComponent = recordComponent;
4449
}
4550

4651
@Override
@@ -59,6 +64,15 @@ protected Object resolveDefaultValue(MetadataGenerationEnvironment environment)
5964
return getSource().asType().accept(DefaultPrimitiveTypeVisitor.INSTANCE, null);
6065
}
6166

67+
@Override
68+
protected String resolveDescription(MetadataGenerationEnvironment environment) {
69+
// record components descriptions are written using @param tag
70+
if (this.recordComponent != null) {
71+
return environment.getTypeUtils().getJavaDoc(this.recordComponent);
72+
}
73+
return super.resolveDescription(environment);
74+
}
75+
6276
private Object getDefaultValueFromAnnotation(MetadataGenerationEnvironment environment, Element element) {
6377
AnnotationMirror annotation = environment.getDefaultValueAnnotation(element);
6478
List<String> defaultValue = getDefaultValue(environment, annotation);

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ private String resolveType(MetadataGenerationEnvironment environment) {
155155
return environment.getTypeUtils().getType(getOwnerElement(), getType());
156156
}
157157

158-
private String resolveDescription(MetadataGenerationEnvironment environment) {
158+
protected String resolveDescription(MetadataGenerationEnvironment environment) {
159159
return environment.getTypeUtils().getJavaDoc(getField());
160160
}
161161

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import javax.lang.model.element.ExecutableElement;
2727
import javax.lang.model.element.Modifier;
2828
import javax.lang.model.element.NestingKind;
29+
import javax.lang.model.element.RecordComponentElement;
2930
import javax.lang.model.element.TypeElement;
3031
import javax.lang.model.element.VariableElement;
3132
import javax.lang.model.type.TypeMirror;
@@ -36,6 +37,7 @@
3637
*
3738
* @author Stephane Nicoll
3839
* @author Phillip Webb
40+
* @author Pavel Anisimov
3941
*/
4042
class PropertyDescriptorResolver {
4143

@@ -82,8 +84,9 @@ Stream<PropertyDescriptor<?>> resolveConstructorProperties(TypeElement type, Typ
8284
ExecutableElement getter = members.getPublicGetter(name, propertyType);
8385
ExecutableElement setter = members.getPublicSetter(name, propertyType);
8486
VariableElement field = members.getFields().get(name);
87+
RecordComponentElement recordComponent = members.getRecordComponents().get(name);
8588
register(candidates, new ConstructorParameterPropertyDescriptor(type, null, parameter, name, propertyType,
86-
field, getter, setter));
89+
field, recordComponent, getter, setter));
8790
});
8891
return candidates.values().stream();
8992
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import javax.lang.model.element.Element;
2828
import javax.lang.model.element.ExecutableElement;
2929
import javax.lang.model.element.Modifier;
30+
import javax.lang.model.element.RecordComponentElement;
3031
import javax.lang.model.element.TypeElement;
3132
import javax.lang.model.element.VariableElement;
3233
import javax.lang.model.type.TypeKind;
@@ -39,6 +40,7 @@
3940
* @author Stephane Nicoll
4041
* @author Phillip Webb
4142
* @author Moritz Halbritter
43+
* @author Pavel Anisimov
4244
*/
4345
class TypeElementMembers {
4446

@@ -54,6 +56,8 @@ class TypeElementMembers {
5456

5557
private final Map<String, VariableElement> fields = new LinkedHashMap<>();
5658

59+
private final Map<String, RecordComponentElement> recordComponents = new LinkedHashMap<>();
60+
5761
private final Map<String, List<ExecutableElement>> publicGetters = new LinkedHashMap<>();
5862

5963
private final Map<String, List<ExecutableElement>> publicSetters = new LinkedHashMap<>();
@@ -72,6 +76,9 @@ private void process(TypeElement element) {
7276
for (ExecutableElement method : ElementFilter.methodsIn(element.getEnclosedElements())) {
7377
processMethod(method);
7478
}
79+
for (RecordComponentElement recordComponent : ElementFilter.recordComponentsIn(element.getEnclosedElements())) {
80+
processRecordComponent(recordComponent);
81+
}
7582
Element superType = this.env.getTypeUtils().asElement(element.getSuperclass());
7683
if (superType instanceof TypeElement && !OBJECT_CLASS_NAME.equals(superType.toString())
7784
&& !RECORD_CLASS_NAME.equals(superType.toString())) {
@@ -189,10 +196,21 @@ private void processField(VariableElement field) {
189196
this.fields.putIfAbsent(name, field);
190197
}
191198

199+
private void processRecordComponent(RecordComponentElement recordComponent) {
200+
String name = recordComponent.getSimpleName().toString();
201+
if (!this.recordComponents.containsKey(name)) {
202+
this.recordComponents.put(name, recordComponent);
203+
}
204+
}
205+
192206
Map<String, VariableElement> getFields() {
193207
return Collections.unmodifiableMap(this.fields);
194208
}
195209

210+
Map<String, RecordComponentElement> getRecordComponents() {
211+
return Collections.unmodifiableMap(this.recordComponents);
212+
}
213+
196214
Map<String, List<ExecutableElement>> getPublicGetters() {
197215
return Collections.unmodifiableMap(this.publicGetters);
198216
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@
2424
import java.util.List;
2525
import java.util.Map;
2626
import java.util.Map.Entry;
27+
import java.util.regex.Matcher;
2728
import java.util.regex.Pattern;
2829
import java.util.stream.Collectors;
2930

3031
import javax.annotation.processing.ProcessingEnvironment;
3132
import javax.lang.model.element.Element;
33+
import javax.lang.model.element.RecordComponentElement;
3234
import javax.lang.model.element.TypeElement;
3335
import javax.lang.model.type.ArrayType;
3436
import javax.lang.model.type.DeclaredType;
@@ -44,6 +46,7 @@
4446
*
4547
* @author Stephane Nicoll
4648
* @author Phillip Webb
49+
* @author Pavel Anisimov
4750
*/
4851
class TypeUtils {
4952

@@ -176,6 +179,9 @@ boolean isCollectionOrMap(TypeMirror type) {
176179
}
177180

178181
String getJavaDoc(Element element) {
182+
if (element instanceof RecordComponentElement) {
183+
return getJavaDoc((RecordComponentElement) element);
184+
}
179185
String javadoc = (element != null) ? this.env.getElementUtils().getDocComment(element) : null;
180186
if (javadoc != null) {
181187
javadoc = NEW_LINE_PATTERN.matcher(javadoc).replaceAll("").trim();
@@ -247,6 +253,24 @@ private void process(TypeDescriptor descriptor, TypeMirror type) {
247253
}
248254
}
249255

256+
private String getJavaDoc(RecordComponentElement recordComponent) {
257+
String recordJavadoc = this.env.getElementUtils().getDocComment(recordComponent.getEnclosingElement());
258+
if (recordJavadoc != null) {
259+
Pattern paramJavadocPattern = paramJavadocPattern(recordComponent.getSimpleName().toString());
260+
Matcher paramJavadocMatcher = paramJavadocPattern.matcher(recordJavadoc);
261+
if (paramJavadocMatcher.find()) {
262+
String paramJavadoc = NEW_LINE_PATTERN.matcher(paramJavadocMatcher.group()).replaceAll("").trim();
263+
return paramJavadoc.isEmpty() ? null : paramJavadoc;
264+
}
265+
}
266+
return null;
267+
}
268+
269+
private Pattern paramJavadocPattern(String paramName) {
270+
String pattern = String.format("(?<=@param +%s).*?(?=([\r\n]+ *@)|$)", paramName);
271+
return Pattern.compile(pattern, Pattern.DOTALL);
272+
}
273+
250274
/**
251275
* A visitor that extracts the fully qualified name of a type, including generic
252276
* information.

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
2323
import org.springframework.boot.configurationprocessor.metadata.Metadata;
2424
import org.springframework.boot.configurationsample.deprecation.Dbcp2Configuration;
25+
import org.springframework.boot.configurationsample.record.ExampleRecord;
2526
import org.springframework.boot.configurationsample.record.RecordWithGetter;
2627
import org.springframework.boot.configurationsample.recursive.RecursiveProperties;
2728
import org.springframework.boot.configurationsample.simple.ClassWithNestedProperties;
@@ -516,4 +517,19 @@ void shouldNotMarkDbcp2UsernameOrPasswordAsDeprecated() {
516517
assertThat(metadata).has(Metadata.withProperty("spring.datasource.dbcp2.password").withNoDeprecation());
517518
}
518519

520+
@Test
521+
void recordPropertiesWithDescriptions() {
522+
ConfigurationMetadata metadata = compile(ExampleRecord.class);
523+
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-string", String.class)
524+
.withDescription("very long description that doesn't fit single line"));
525+
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-integer", Integer.class)
526+
.withDescription("description with @param and @ pitfalls"));
527+
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-boolean", Boolean.class)
528+
.withDescription("description with extra spaces"));
529+
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-long", Long.class)
530+
.withDescription("description without space after asterisk"));
531+
assertThat(metadata).has(Metadata.withProperty("record.descriptions.some-byte", Byte.class)
532+
.withDescription("last description in Javadoc"));
533+
}
534+
519535
}

spring-boot-project/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ void constructorParameterDeprecatedPropertyOnGetter() {
131131
VariableElement field = getField(ownerElement, "flag");
132132
VariableElement constructorParameter = getConstructorParameter(ownerElement, "flag");
133133
ConstructorParameterPropertyDescriptor property = new ConstructorParameterPropertyDescriptor(ownerElement,
134-
null, constructorParameter, "flag", field.asType(), field, getter, null);
134+
null, constructorParameter, "flag", field.asType(), field, null, getter, null);
135135
assertItemMetadata(metadataEnv, property).isProperty().isDeprecatedWithNoInformation();
136136
});
137137
}
@@ -223,7 +223,7 @@ protected ConstructorParameterPropertyDescriptor createPropertyDescriptor(TypeEl
223223
ExecutableElement getter = getMethod(ownerElement, createAccessorMethodName("get", name));
224224
ExecutableElement setter = getMethod(ownerElement, createAccessorMethodName("set", name));
225225
return new ConstructorParameterPropertyDescriptor(ownerElement, null, constructorParameter, name,
226-
field.asType(), field, getter, setter);
226+
field.asType(), field, null, getter, setter);
227227
}
228228

229229
private VariableElement getConstructorParameter(TypeElement ownerElement, String name) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.configurationsample.record;
18+
19+
/**
20+
* Example Record Javadoc sample
21+
*
22+
* @param someString very long description that doesn't fit single line
23+
* @param someInteger description with @param and @ pitfalls
24+
* @param someBoolean description with extra spaces
25+
* @param someLong description without space after asterisk
26+
* @param someByte last description in Javadoc
27+
* @since 1.0.0
28+
* @author Pavel Anisimov
29+
*/
30+
@org.springframework.boot.configurationsample.ConfigurationProperties("record.descriptions")
31+
public record ExampleRecord(String someString, Integer someInteger, Boolean someBoolean, Long someLong, Byte someByte) {
32+
}

0 commit comments

Comments
 (0)