Skip to content

Commit e3787e5

Browse files
authored
Added support for @value javadoc reference to resolve into value in docs (#10759)
* Added support for `@value` javadoc reference to resolve into the actual value of the constant, if in the same class, or the class is in the same package, or the reference is fully qualified. * Update test to verify that the new approach works (we expected constant name, now it has constant value)
1 parent b89efa3 commit e3787e5

File tree

9 files changed

+153
-60
lines changed

9 files changed

+153
-60
lines changed

codegen/apt/src/main/java/io/helidon/codegen/apt/AptTypeInfoFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ public static Optional<TypedElementInfo> createTypedElementInfoFromElement(AptCo
263263
}
264264
} else if (v instanceof VariableElement ve) {
265265
typeMirror = Objects.requireNonNull(ve.asType());
266+
Object variableDefault = ve.getConstantValue();
267+
// configure default value for constants
268+
if (variableDefault != null) {
269+
defaultValue = String.valueOf(variableDefault);
270+
}
266271
}
267272

268273
if (typeMirror != null) {

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfigMetadataCodegenExtension.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2024, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -73,7 +73,7 @@ public void processingOver(RoundContext roundContext) {
7373
.map(it -> TypeHandlerBuilderApi.create(ctx, it)),
7474
typesToProcess(configMetadata)
7575
.map(it -> TypeHandlerMetaApi.create(ctx, it)))
76-
.map(TypeHandler::handle)
76+
.map(it -> it.handle(roundContext))
7777
.forEach(it -> {
7878
TypeName targetType = it.targetType();
7979
newOptions.put(targetType, it.configuredType());

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/ConfiguredOptionData.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import java.util.function.Predicate;
2323

2424
import io.helidon.codegen.CodegenContext;
25+
import io.helidon.codegen.RoundContext;
2526
import io.helidon.common.types.Annotation;
2627
import io.helidon.common.types.TypeInfo;
2728
import io.helidon.common.types.TypeName;
@@ -63,7 +64,7 @@ final class ConfiguredOptionData {
6364
private TypeName providerType;
6465

6566
// create from @ConfiguredOption in config-metadata
66-
static ConfiguredOptionData createMeta(CodegenContext ctx, Annotation option) {
67+
static ConfiguredOptionData createMeta(CodegenContext ctx, RoundContext roundContext, Annotation option) {
6768
ConfiguredOptionData result = new ConfiguredOptionData();
6869

6970
option.booleanValue("configured").ifPresent(result::configured);
@@ -90,7 +91,7 @@ static ConfiguredOptionData createMeta(CodegenContext ctx, Annotation option) {
9091
.map(TypeName::create)
9192
.flatMap(ctx::typeInfo)
9293
.filter(it -> it.kind() == ENUM)
93-
.ifPresent(it -> enumAllowedValues(result.allowedValues(), it));
94+
.ifPresent(it -> enumAllowedValues(roundContext, result.allowedValues(), it));
9495
}
9596

9697
return result;
@@ -151,13 +152,13 @@ static ConfiguredOptionData createBuilder(TypedElementInfo element) {
151152
return result;
152153
}
153154

154-
static void enumAllowedValues(List<AllowedValue> allowedValues, TypeInfo typeInfo) {
155+
static void enumAllowedValues(RoundContext roundContext, List<AllowedValue> allowedValues, TypeInfo typeInfo) {
155156
typeInfo.elementInfo()
156157
.stream()
157158
.filter(it -> it.kind() == ENUM_CONSTANT)
158159
.forEach(it -> {
159160
allowedValues.add(new AllowedValue(it.elementName(), it.description()
160-
.map(Javadoc::parse)
161+
.map(javadoc -> Javadoc.parse(roundContext, typeInfo, javadoc))
161162
.orElse("")));
162163
});
163164
}

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/Javadoc.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
import java.util.regex.Pattern;
2020

21+
import io.helidon.codegen.ElementInfoPredicates;
22+
import io.helidon.codegen.RoundContext;
23+
import io.helidon.common.types.TypeInfo;
24+
import io.helidon.common.types.TypeName;
25+
import io.helidon.common.types.TypedElementInfo;
26+
2127
import static io.helidon.codegen.CodegenUtil.capitalize;
2228

2329
/*
@@ -38,8 +44,8 @@ private Javadoc() {
3844
}
3945

4046
// for existing usages
41-
static String parse(String javadoc) {
42-
return parse(javadoc, true);
47+
static String parse(RoundContext roundContext, TypeInfo currentType, String javadoc) {
48+
return parse(roundContext, currentType, javadoc, true);
4349
}
4450

4551
/**
@@ -60,7 +66,7 @@ static String parse(String javadoc) {
6066
* @param docComment "raw" javadoc from the source code
6167
* @return description of the option
6268
*/
63-
static String parse(String docComment, boolean includeReturn) {
69+
static String parse(RoundContext roundContext, TypeInfo currentType, String docComment, boolean includeReturn) {
6470
if (docComment == null) {
6571
return "";
6672
}
@@ -77,7 +83,8 @@ static String parse(String docComment, boolean includeReturn) {
7783
// replace all {@link ...} with just the name
7884
javadoc = JAVADOC_LINKPLAIN.matcher(javadoc).replaceAll(it -> javadocLink(it.group(1)));
7985
// replace all {@value ...} with just the reference
80-
javadoc = JAVADOC_VALUE.matcher(javadoc).replaceAll(it -> javadocValue(it.group(1)));
86+
javadoc = JAVADOC_VALUE.matcher(javadoc)
87+
.replaceAll(it -> javadocConstantValue(roundContext, currentType, it.group(1)));
8188

8289
int count = 9;
8390
index = javadoc.indexOf(" @return");
@@ -134,4 +141,54 @@ private static String javadocValue(String originalValue) {
134141
}
135142
return originalValue.replace('#', '.');
136143
}
144+
145+
private static String javadocConstantValue(RoundContext roundContext, TypeInfo currentType, String originalValue) {
146+
if (originalValue.startsWith("#")) {
147+
// constant is in this class
148+
String constantName = originalValue.substring(1);
149+
150+
return constantValue(currentType, originalValue, constantName);
151+
} else {
152+
int separator = originalValue.lastIndexOf('#');
153+
if (separator < 0) {
154+
return javadocValue(originalValue);
155+
}
156+
TypeName typeName = TypeName.create(originalValue.substring(0, separator));
157+
if (typeName.packageName().isEmpty()) {
158+
typeName = TypeName.builder(typeName)
159+
.packageName(currentType.typeName().packageName())
160+
.build();
161+
}
162+
String constantName = originalValue.substring(separator + 1);
163+
// constant is in a different class, if there is no package information, we are in trouble - we will consider
164+
// this to be in this package, otherwise we will just use the value as present in the javadoc
165+
var constantType = roundContext.typeInfo(typeName);
166+
if (constantType.isEmpty()) {
167+
return javadocValue(originalValue);
168+
}
169+
170+
return constantValue(constantType.get(), originalValue, constantName);
171+
}
172+
}
173+
174+
private static String constantValue(TypeInfo currentType, String originalValue, String constantName) {
175+
var fieldValue = currentType.elementInfo()
176+
.stream()
177+
.filter(ElementInfoPredicates::isField)
178+
.filter(ElementInfoPredicates::isStatic)
179+
.filter(ElementInfoPredicates.elementName(constantName))
180+
.findFirst()
181+
.flatMap(TypedElementInfo::defaultValue);
182+
183+
if (fieldValue.isEmpty()) {
184+
return javadocValue(originalValue);
185+
}
186+
var constantValue = fieldValue.get();
187+
// this is a value
188+
// if it contains `, we must replace it
189+
// if it contains $, we must escape it
190+
constantValue = constantValue.replace('`', '"');
191+
constantValue = constantValue.replaceAll("\\$", "\\\\\\$");
192+
return "`" + constantValue + "`";
193+
}
137194
}

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandler.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2025 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,11 @@
1616

1717
package io.helidon.config.metadata.codegen;
1818

19+
import io.helidon.codegen.RoundContext;
20+
1921
interface TypeHandler {
2022
/**
2123
* Discover all options of the configured type.
2224
*/
23-
TypeHandlerResult handle();
25+
TypeHandlerResult handle(RoundContext roundContext);
2426
}

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBase.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.function.Predicate;
2323

2424
import io.helidon.codegen.CodegenContext;
25+
import io.helidon.codegen.RoundContext;
2526
import io.helidon.common.types.TypeInfo;
2627
import io.helidon.common.types.TypeName;
2728
import io.helidon.common.types.TypedElementInfo;
@@ -79,8 +80,8 @@ static String toConfigKey(String methodName) {
7980
return result.toString();
8081
}
8182

82-
String javadoc(String docComment) {
83-
return Javadoc.parse(docComment);
83+
String javadoc(RoundContext roundContext, TypeInfo currentType, String docComment) {
84+
return Javadoc.parse(roundContext, currentType, docComment);
8485
}
8586

8687
String key(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) {
@@ -91,10 +92,13 @@ String key(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption)
9192
return name;
9293
}
9394

94-
String description(TypedElementInfo elementInfo, ConfiguredOptionData configuredOption) {
95+
String description(RoundContext roundContext,
96+
TypeInfo typeInfo,
97+
TypedElementInfo elementInfo,
98+
ConfiguredOptionData configuredOption) {
9599
String desc = configuredOption.description();
96100
if (desc == null) {
97-
return javadoc(elementInfo.description().orElse(null));
101+
return javadoc(roundContext, typeInfo, elementInfo.description().orElse(null));
98102
}
99103
return desc;
100104
}
@@ -103,13 +107,15 @@ String defaultValue(String defaultValue) {
103107
return UNCONFIGURED_OPTION.equals(defaultValue) ? null : defaultValue;
104108
}
105109

106-
List<ConfiguredOptionData.AllowedValue> allowedValues(ConfiguredOptionData configuredOption, TypeName type) {
110+
List<ConfiguredOptionData.AllowedValue> allowedValues(RoundContext roundContext,
111+
ConfiguredOptionData configuredOption,
112+
TypeName type) {
107113
if (type.equals(configuredOption.type()) || !configuredOption.allowedValues().isEmpty()) {
108114
// this was already processed due to an explicit type defined in the annotation
109115
// or allowed values explicitly configured in annotation
110116
return configuredOption.allowedValues();
111117
}
112-
return allowedValues(type);
118+
return allowedValues(roundContext, type);
113119
}
114120

115121
CodegenContext ctx() {
@@ -156,25 +162,27 @@ void addSuperClasses(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAn
156162
}
157163
}
158164

159-
List<ConfiguredOptionData.AllowedValue> allowedValuesEnum(ConfiguredOptionData data, TypeInfo enumInfo) {
165+
List<ConfiguredOptionData.AllowedValue> allowedValuesEnum(RoundContext roundContext,
166+
ConfiguredOptionData data,
167+
TypeInfo enumInfo) {
160168
if (!data.allowedValues().isEmpty()) {
161169
// this was already processed due to an explicit type defined in the annotation
162170
// or allowed values explicitly configured in annotation
163171
return data.allowedValues();
164172
}
165-
return allowedValuesEnum(enumInfo);
173+
return allowedValuesEnum(roundContext, enumInfo);
166174
}
167175

168-
private List<ConfiguredOptionData.AllowedValue> allowedValuesEnum(TypeInfo enumInfo) {
176+
private List<ConfiguredOptionData.AllowedValue> allowedValuesEnum(RoundContext roundContext, TypeInfo enumInfo) {
169177
List<ConfiguredOptionData.AllowedValue> values = new ArrayList<>();
170-
ConfiguredOptionData.enumAllowedValues(values, enumInfo);
178+
ConfiguredOptionData.enumAllowedValues(roundContext, values, enumInfo);
171179
return values;
172180
}
173181

174-
private List<ConfiguredOptionData.AllowedValue> allowedValues(TypeName type) {
182+
private List<ConfiguredOptionData.AllowedValue> allowedValues(RoundContext roundContext, TypeName type) {
175183
return ctx().typeInfo(type)
176184
.filter(it -> it.kind() == ENUM)
177-
.map(this::allowedValuesEnum)
185+
.map(it -> allowedValuesEnum(roundContext, it))
178186
.orElseGet(List::of);
179187

180188
}

config/metadata/codegen/src/main/java/io/helidon/config/metadata/codegen/TypeHandlerBuilderApi.java

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.helidon.codegen.CodegenContext;
2424
import io.helidon.codegen.CodegenException;
2525
import io.helidon.codegen.ElementInfoPredicates;
26+
import io.helidon.codegen.RoundContext;
2627
import io.helidon.common.types.TypeInfo;
2728
import io.helidon.common.types.TypeName;
2829
import io.helidon.common.types.TypedElementInfo;
@@ -59,7 +60,7 @@ static TypeHandler create(CodegenContext ctx, TypeInfo typeInfo) {
5960
- return type is the one to use
6061
*/
6162
@Override
62-
public TypeHandlerResult handle() {
63+
public TypeHandlerResult handle(RoundContext roundContext) {
6364
TypeName prototype = prototype(blueprintType);
6465
TypeName builderType = TypeName.builder(prototype)
6566
.className("Builder")
@@ -95,7 +96,9 @@ public TypeHandlerResult handle() {
9596
.filter(Predicate.not(ElementInfoPredicates::isStatic))
9697
.filter(Predicate.not(ElementInfoPredicates::isDefault))
9798
.filter(ElementInfoPredicates.hasAnnotation(OPTION_CONFIGURED))
98-
.forEach(it -> processBlueprintMethod(builderType,
99+
.forEach(it -> processBlueprintMethod(roundContext,
100+
blueprint,
101+
builderType,
99102
type,
100103
it));
101104

@@ -127,6 +130,12 @@ void addInterfaces(ConfiguredType type, TypeInfo typeInfo, TypeName requiredAnno
127130
}
128131
}
129132

133+
// for blueprints, we only want the description, not the return information (as it duplicates information)
134+
@Override
135+
String javadoc(RoundContext roundContext, TypeInfo currentType, String docComment) {
136+
return Javadoc.parse(roundContext, currentType, docComment, false);
137+
}
138+
130139
private static TypeName prototype(TypeName blueprintType) {
131140
String className = blueprintType.className();
132141
if (className.endsWith("Blueprint")) {
@@ -170,13 +179,13 @@ private OptionType typeForBlueprintFromSignature(TypedElementInfo element,
170179
if (returnType.isOptional()) {
171180
// may be an optional of list etc.
172181
if (!(returnType.isMap() || returnType.isSet() || returnType.isList())) {
173-
return new OptionType(returnType.typeArguments().get(0), "VALUE");
182+
return new OptionType(returnType.typeArguments().getFirst(), "VALUE");
174183
}
175-
returnType = returnType.typeArguments().get(0);
184+
returnType = returnType.typeArguments().getFirst();
176185
}
177186

178187
if (returnType.isList() || returnType.isSet()) {
179-
return new OptionType(returnType.typeArguments().get(0), "LIST");
188+
return new OptionType(returnType.typeArguments().getFirst(), "LIST");
180189
}
181190

182191
if (returnType.isMap()) {
@@ -186,12 +195,16 @@ private OptionType typeForBlueprintFromSignature(TypedElementInfo element,
186195
return new OptionType(returnType.boxed(), annotation.kind());
187196
}
188197

189-
private void processBlueprintMethod(TypeName typeName, ConfiguredType configuredType, TypedElementInfo elementInfo) {
198+
private void processBlueprintMethod(RoundContext roundContext,
199+
TypeInfo currentType,
200+
TypeName typeName,
201+
ConfiguredType configuredType,
202+
TypedElementInfo elementInfo) {
190203
// we always have exactly one option per method
191204
ConfiguredOptionData data = ConfiguredOptionData.createBuilder(elementInfo);
192205

193206
String name = key(elementInfo, data);
194-
String description = description(elementInfo, data);
207+
String description = description(roundContext, currentType, elementInfo, data);
195208
String defaultValue = defaultValue(data.defaultValue());
196209
boolean experimental = data.experimental();
197210
OptionType type = typeForBlueprintFromSignature(elementInfo, data);
@@ -206,9 +219,9 @@ private void processBlueprintMethod(TypeName typeName, ConfiguredType configured
206219
if (enumType.isPresent() && defaultValue != null) {
207220
// prefix the default value with the enum name to make it more readable
208221
defaultValue = type.elementType().className() + "." + defaultValue;
209-
allowedValues = allowedValuesEnum(data, enumType.get());
222+
allowedValues = allowedValuesEnum(roundContext, data, enumType.get());
210223
} else {
211-
allowedValues = allowedValues(data, type.elementType());
224+
allowedValues = allowedValues(roundContext, data, type.elementType());
212225
}
213226

214227
List<TypeName> paramTypes = List.of(elementInfo.typeName());
@@ -233,10 +246,4 @@ private void processBlueprintMethod(TypeName typeName, ConfiguredType configured
233246
allowedValues);
234247
configuredType.addProperty(property);
235248
}
236-
237-
// for blueprints, we only want the description, not the return information (as it duplicates information)
238-
@Override
239-
String javadoc(String docComment) {
240-
return Javadoc.parse(docComment, false);
241-
}
242249
}

0 commit comments

Comments
 (0)