Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import javax.tools.Diagnostic;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand Down Expand Up @@ -133,15 +132,15 @@ public boolean run(DocletEnvironment environment) {
try {
return doRun(environment);
} catch (RuntimeException e) {
reporter.print(Diagnostic.Kind.ERROR, "Error running ConfigurationCollectorDoclet: " + e.getMessage());
e.printStackTrace(reporter.getStandardWriter());
reportError("Error running ConfigurationCollectorDoclet: " + e.getMessage());
return false;
}
}

private boolean doRun(DocletEnvironment environment) {
if (output == null) {
throw new IllegalStateException("Missing required --output option");
reportError("Missing required --output option");
return false;
}
docTrees = environment.getDocTrees();
List<Map<String, String>> discoveredKeys = new ArrayList<>();
Expand All @@ -153,43 +152,85 @@ private boolean doRun(DocletEnvironment environment) {
continue;
}
DocCommentTree docComment = docTrees.getDocCommentTree(field);
if ("maven".equals(mode)) {
processMavenField(type, field, docComment, discoveredKeys);
} else if ("resolver".equals(mode)) {
processResolverField(type, field, docComment, discoveredKeys);
} else {
throw new IllegalArgumentException("Unknown mode: " + mode);
try {
if ("maven".equals(mode)) {
processMavenField(type, field, docComment, discoveredKeys);
} else if ("resolver".equals(mode)) {
processResolverField(type, field, docComment, discoveredKeys);
} else {
// TODO: move to beginning of run() and validate mode before processing any types
reportError("Unknown mode: " + mode);
return false;
}
} catch (DocTreePathAwareRuntimeException e) {
reportError(e.getDocTreePath(), e.getMessage());
} catch (RuntimeException e) {
DocTreePath rootPath = new DocTreePath(docTrees.getPath(field), docComment);
reportError(rootPath, e.getMessage());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low-severity: The DocTreePath constructor calls Objects.requireNonNull on the DocCommentTree parameter, so if docComment is null when a RuntimeException is caught here, the catch block itself would NPE and mask the original error.

In resolver mode this is unreachable (early return on null docComment), but processMavenField does not guard against null docComment in the same way. Interestingly, buildTagPath (line 355–361 in this PR) already applies exactly this defensive pattern.

Suggested change
reportError(rootPath, e.getMessage());
} catch (RuntimeException e) {
DocTreePath rootPath =
docComment != null ? new DocTreePath(docTrees.getPath(field), docComment) : null;
reportError(rootPath, e.getMessage());

}
}
}

writeProperties(discoveredKeys);
try {
writeProperties(discoveredKeys);
} catch (IOException e) {
reportError("Failed to write properties file: " + e.getMessage());
return false;
}
return true;
}

/**
* Reports an error message at a specific DocTreePath location.
*
* @param path the DocTreePath where the error occurred
* @param message the error message
*/
private void reportError(DocTreePath path, String message) {
if (path != null) {
reporter.print(Diagnostic.Kind.ERROR, path, message);
} else {
reportError(message);
}
}

/**
* Reports a global error message without location information.
*
* @param message the error message
*/
private void reportError(String message) {
reporter.print(Diagnostic.Kind.ERROR, message);
}

private void processResolverField(
TypeElement type, VariableElement field, DocCommentTree docComment, List<Map<String, String>> discovered) {
if (docComment == null) {
return;
}
Map<String, List<? extends DocTree>> blockTags = collectBlockTags(docComment);
Map<String, UnknownBlockTagTree> blockTags = collectBlockTags(docComment);
if (!blockTags.containsKey("configurationSource")) {
return;
}

String configurationType =
getConfigurationType(extractClassLink(field, docComment, blockTags.get("configurationType")));
getConfigurationType(extractClassLink(field, docComment, blockTags, "configurationType"));
String defValue = resolveDefaultValue(type, field, docComment, blockTags.get("configurationDefaultValue"));

UnknownBlockTagTree sourceTag = blockTags.get("configurationSource");
UnknownBlockTagTree repoIdTag = blockTags.get("configurationRepoIdSuffix");

Map<String, String> entry = new LinkedHashMap<>();
entry.put("key", String.valueOf(field.getConstantValue()));
entry.put("defaultValue", nvl(defValue, ""));
entry.put("fqName", type.getQualifiedName() + "." + field.getSimpleName());
entry.put("description", cleanseJavadoc(renderContent(docComment.getFullBody())));
entry.put("since", nvl(getSince(type, docComment), ""));
entry.put("configurationSource", getConfigurationSource(renderContent(blockTags.get("configurationSource"))));
entry.put(
"configurationSource",
getConfigurationSource(renderContent(sourceTag != null ? sourceTag.getContent() : null)));
entry.put("configurationType", configurationType);
entry.put("supportRepoIdSuffix", toYesNo(renderContent(blockTags.get("configurationRepoIdSuffix"))));
entry.put("supportRepoIdSuffix", toYesNo(renderContent(repoIdTag != null ? repoIdTag.getContent() : null)));
discovered.add(entry);
}

Expand All @@ -216,8 +257,8 @@ private void processMavenField(
Object value = attribute.getValue().getValue();
switch (name) {
case "source":
source = value instanceof VariableElement
? ((VariableElement) value).getSimpleName().toString()
source = value instanceof VariableElement variableElement
? variableElement.getSimpleName().toString()
: String.valueOf(value);
break;
case "defaultValue":
Expand Down Expand Up @@ -278,7 +319,7 @@ private AnnotationMirror getAnnotation(Element element, String fqName) {
return null;
}

private void writeProperties(List<Map<String, String>> discoveredKeys) {
private void writeProperties(List<Map<String, String>> discoveredKeys) throws IOException {
Properties properties = new Properties();
properties.setProperty("keys.count", String.valueOf(discoveredKeys.size()));
for (int i = 0; i < discoveredKeys.size(); i++) {
Expand All @@ -287,42 +328,50 @@ private void writeProperties(List<Map<String, String>> discoveredKeys) {
properties.setProperty("keys." + i + "." + field.getKey(), field.getValue());
}
}
try {
if (output.getParent() != null) {
Files.createDirectories(output.getParent());
}
try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
properties.store(writer, "Generated by ConfigurationCollectorDoclet - DO NOT EDIT");
}
} catch (IOException e) {
throw new UncheckedIOException(e);
if (output.getParent() != null) {
Files.createDirectories(output.getParent());
}
try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) {
properties.store(writer, "Generated by ConfigurationCollectorDoclet - DO NOT EDIT");
}
}

// --- Javadoc extraction helpers -------------------------------------------------------------------------------

private Map<String, List<? extends DocTree>> collectBlockTags(DocCommentTree docComment) {
Map<String, List<? extends DocTree>> result = new LinkedHashMap<>();
private Map<String, UnknownBlockTagTree> collectBlockTags(DocCommentTree docComment) {
Map<String, UnknownBlockTagTree> result = new LinkedHashMap<>();
for (DocTree tag : docComment.getBlockTags()) {
if (tag instanceof UnknownBlockTagTree) {
UnknownBlockTagTree unknown = (UnknownBlockTagTree) tag;
result.put(unknown.getTagName(), unknown.getContent());
if (tag instanceof UnknownBlockTagTree unknownBlockTree) {
result.put(unknownBlockTree.getTagName(), unknownBlockTree);
}
}
return result;
}

/**
* Builds a {@link DocTreePath} pointing to a specific block tag within a field's doc comment, enabling
* precise Javadoc-level error messages via {@link Reporter#print(Diagnostic.Kind, DocTreePath, String)}.
*/
private DocTreePath buildTagPath(VariableElement field, DocCommentTree docComment, UnknownBlockTagTree tag) {
if (field == null || docComment == null || tag == null) {
return null;
}
DocTreePath docCommentPath = new DocTreePath(docTrees.getPath(field), docComment);
return new DocTreePath(docCommentPath, tag);
}

private String resolveDefaultValue(
TypeElement type,
VariableElement contextField,
DocCommentTree docComment,
List<? extends DocTree> content) {
if (content == null || content.isEmpty()) {
TypeElement type, VariableElement contextField, DocCommentTree docComment, UnknownBlockTagTree contentTag) {
if (contentTag == null) {
return null;
}
List<? extends DocTree> content = contentTag.getContent();
if (content.isEmpty()) {
return null;
}
DocTreePath tagPath = buildTagPath(contextField, docComment, contentTag);
for (DocTree tree : content) {
if (tree instanceof LinkTree) {
LinkTree link = (LinkTree) tree;
if (tree instanceof LinkTree link) {
if (link.getReference() != null) {
String signature = link.getReference().getSignature();
// resolve the referenced constant using the fully qualified signature, so that references
Expand All @@ -332,9 +381,12 @@ private String resolveDefaultValue(
? lookupConstant(referenced)
: lookupConstant(type, signature.substring(signature.indexOf('#') + 1));
if (value == null) {
// hard fail as in the original implementation: default value constants must be resolvable
throw new IllegalArgumentException("Could not look up {@link " + signature
+ "} for configuration " + type.getQualifiedName());
// hard fail: default value constants must be resolvable; report at the precise
// link-reference location if we can resolve a path to it, otherwise at the block tag
DocTreePath rootPath = new DocTreePath(docTrees.getPath(contextField), docComment);
DocTreePath linkRefPath = DocTreePath.getPath(rootPath, link.getReference());
throw new DocTreePathAwareRuntimeException(
linkRefPath != null ? linkRefPath : tagPath, "Could not resolve link: " + signature);
}
return value;
}
Expand All @@ -359,7 +411,7 @@ private VariableElement resolveReferencedField(
return null;
}
Element element = docTrees.getElement(refPath);
return element instanceof VariableElement ? (VariableElement) element : null;
return element instanceof VariableElement variableElement ? variableElement : null;
}

private String lookupConstant(TypeElement type, String constantName) {
Expand Down Expand Up @@ -417,23 +469,32 @@ private String resolveEnumReference(VariableElement field) {
}

private String extractClassLink(
VariableElement contextField, DocCommentTree docComment, List<? extends DocTree> content) {
if (content == null || content.isEmpty()) {
throw new IllegalArgumentException("Missing content for @configurationDefaultValue");
VariableElement contextField,
DocCommentTree docComment,
Map<String, UnknownBlockTagTree> blockTags,
String tagName) {
UnknownBlockTagTree tag = blockTags.get(tagName);
if (tag == null || tag.getContent().isEmpty()) {

throw new IllegalArgumentException("Missing content for @" + tagName);
}
for (DocTree tree : content) {
DocTreePath tagPath = buildTagPath(contextField, docComment, tag);
for (DocTree tree : tag.getContent()) {
// just use the first link, ignore any other content (e.g. text) in the tag
if (tree instanceof LinkTree link) {
String signature = link.getReference().getSignature();
if (signature.contains("#")) {
throw new IllegalArgumentException(
"Expected a class link in @configurationDefaultValue, but got a member reference: "
+ signature);
// report at the precise link reference node within the block tag
DocTreePath rootPath = new DocTreePath(docTrees.getPath(contextField), docComment);
DocTreePath linkRefPath = DocTreePath.getPath(rootPath, link.getReference());
throw new DocTreePathAwareRuntimeException(
linkRefPath != null ? linkRefPath : tagPath,
"Expected a class link in @" + tagName + ", but got a member reference: " + signature);
}
return resolveReferencedType(contextField, docComment, link, signature);
}
}
throw new IllegalArgumentException("No valid {@link ...} reference found in @configurationDefaultValue");
throw new DocTreePathAwareRuntimeException(tagPath, "No valid {@link ...} reference found in @" + tagName);
}

/**
Expand All @@ -452,8 +513,8 @@ private String resolveReferencedType(
return signature;
}
Element element = docTrees.getElement(refPath);
return element instanceof TypeElement
? ((TypeElement) element).getQualifiedName().toString()
return element instanceof TypeElement typeElement
? typeElement.getQualifiedName().toString()
: signature;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* http://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.eclipse.aether.tools;

import com.sun.source.util.DocTreePath;

public class DocTreePathAwareRuntimeException extends RuntimeException {

private static final long serialVersionUID = -4295354135012887795L;
private final DocTreePath docTreePath;

public DocTreePathAwareRuntimeException(DocTreePath docTreePath, String message) {
super(message);
this.docTreePath = docTreePath;
}

public DocTreePathAwareRuntimeException(DocTreePath docTreePath, String message, Throwable cause) {
super(message, cause);
this.docTreePath = docTreePath;
}

public DocTreePath getDocTreePath() {
return docTreePath;
}
}
Loading
Loading