This is an automated email from the ASF dual-hosted git repository.
kwin pushed a commit to branch bugfix/improve-error-reporting-of-doclet
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git
The following commit(s) were added to
refs/heads/bugfix/improve-error-reporting-of-doclet by this push:
new 501691d82 Improve error reporting
501691d82 is described below
commit 501691d828ef2d28521353a88b5f003a99badf55
Author: Konrad Windszus <[email protected]>
AuthorDate: Mon Jul 20 14:08:58 2026 +0200
Improve error reporting
Add unit tests to check error reporting
---
.../aether/tools/ConfigurationCollectorDoclet.java | 126 ++++++++++-----------
.../tools/DocTreePathAwareRuntimeException.java | 41 +++++++
.../tools/ConfigurationCollectorDocletTest.java | 121 +++++++++++++++++---
.../sample/InvalidSampleConfigurationKeys.java | 55 +++++++++
4 files changed, 265 insertions(+), 78 deletions(-)
diff --git
a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
index 7aea2fe2a..7a5e1370f 100644
---
a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
+++
b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/ConfigurationCollectorDoclet.java
@@ -159,12 +159,15 @@ public class ConfigurationCollectorDoclet implements
Doclet {
} else if ("resolver".equals(mode)) {
processResolverField(type, field, docComment,
discoveredKeys);
} else {
- reportError(field, "Unknown mode: " + mode);
+ // TODO: move to beginning of run() and validate mode
before processing any types
+ reportError("Unknown mode: " + mode);
+ return false;
}
- } catch (IllegalArgumentException e) {
- reportError(field, e.getMessage());
+ } catch (DocTreePathAwareRuntimeException e) {
+ reportError(e.getDocTreePath(), e.getMessage());
} catch (RuntimeException e) {
- reportError(field, "Failed to process: " + e.getMessage());
+ DocTreePath rootPath = new
DocTreePath(docTrees.getPath(field), docComment);
+ reportError(rootPath, e.getMessage());
}
}
}
@@ -178,20 +181,6 @@ public class ConfigurationCollectorDoclet implements
Doclet {
return true;
}
- /**
- * Reports an error message at a specific element location.
- *
- * @param element the element where the error occurred
- * @param message the error message
- */
- private void reportError(Element element, String message) {
- if (element != null) {
- reporter.print(Diagnostic.Kind.ERROR, element, message);
- } else {
- reportError(message);
- }
- }
-
/**
* Reports an error message at a specific DocTreePath location.
*
@@ -220,30 +209,34 @@ public class ConfigurationCollectorDoclet implements
Doclet {
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,
"configurationType"));
- String defValue =
- resolveDefaultValue(type, field, docComment,
blockTags.get("configurationDefaultValue"));
+ String configurationType =
+ getConfigurationType(extractClassLink(field, docComment,
blockTags, "configurationType"));
+ String defValue = resolveDefaultValue(type, field, docComment,
blockTags.get("configurationDefaultValue"));
if (defValue == null) {
// Error was already reported, skip this field
return;
}
+ 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);
}
@@ -355,24 +348,38 @@ public class ConfigurationCollectorDoclet implements
Doclet {
// --- 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 unknownBlockTree) {
- result.put(unknownBlockTree.getTagName(),
unknownBlockTree.getContent());
+ 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 link) {
if (link.getReference() != null) {
@@ -384,20 +391,12 @@ public class ConfigurationCollectorDoclet implements
Doclet {
? lookupConstant(referenced)
: lookupConstant(type,
signature.substring(signature.indexOf('#') + 1));
if (value == null) {
- // hard fail: default value constants must be
resolvable
- DocTreePath linkPath =
resolveDocTreePath(contextField, docComment, link);
- if (linkPath != null) {
- reportError(
- linkPath,
- "Could not look up {@link " + signature +
"} for configuration "
- + type.getQualifiedName());
- } else {
- reportError(
- contextField,
- "Could not look up {@link " + signature +
"} for configuration "
- + type.getQualifiedName());
- }
- throw new IllegalArgumentException("Could not resolve
link: " + signature);
+ // 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;
}
@@ -425,18 +424,6 @@ public class ConfigurationCollectorDoclet implements
Doclet {
return element instanceof VariableElement variableElement ?
variableElement : null;
}
- /**
- * Resolves the {@link DocTreePath} for a {@code {@link ...}} reference.
Returns {@code null} if the path cannot
- * be resolved.
- */
- private DocTreePath resolveDocTreePath(VariableElement contextField,
DocCommentTree docComment, LinkTree link) {
- if (contextField == null || docComment == null) {
- return null;
- }
- DocTreePath rootPath = new DocTreePath(docTrees.getPath(contextField),
docComment);
- return DocTreePath.getPath(rootPath, link.getReference());
- }
-
private String lookupConstant(TypeElement type, String constantName) {
for (VariableElement field :
ElementFilter.fieldsIn(type.getEnclosedElements())) {
if (field.getSimpleName().contentEquals(constantName)) {
@@ -492,23 +479,32 @@ public class ConfigurationCollectorDoclet implements
Doclet {
}
private String extractClassLink(
- VariableElement contextField, DocCommentTree docComment,
Map<String, List<? extends DocTree>> blockTags, String tagName) {
- List<? extends DocTree> content = blockTags.get(tagName);
- if (content == null || content.isEmpty()) {
+ 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(
+ // 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 @" + tagName);
+ throw new DocTreePathAwareRuntimeException(tagPath, "No valid {@link
...} reference found in @" + tagName);
}
/**
diff --git
a/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/DocTreePathAwareRuntimeException.java
b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/DocTreePathAwareRuntimeException.java
new file mode 100644
index 000000000..ea3d9bd9b
--- /dev/null
+++
b/maven-resolver-tools/src/main/java/org/eclipse/aether/tools/DocTreePathAwareRuntimeException.java
@@ -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;
+ }
+}
diff --git
a/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java
b/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java
index 238360b08..e315ba560 100644
---
a/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java
+++
b/maven-resolver-tools/src/test/java/org/eclipse/aether/tools/ConfigurationCollectorDocletTest.java
@@ -18,6 +18,8 @@
*/
package org.eclipse.aether.tools;
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticListener;
import javax.tools.DocumentationTool;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
@@ -26,19 +28,24 @@ import javax.tools.ToolProvider;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringWriter;
+import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -50,17 +57,37 @@ class ConfigurationCollectorDocletTest {
*/
private static final String FIXTURE =
"/org/eclipse/aether/sample/SampleConfigurationKeys.java";
- @Test
- void extractsBooleanStringAndEnumConfigurations(@TempDir Path tempDir)
throws Exception {
- Path sourceDir =
Files.createDirectories(tempDir.resolve("org/eclipse/aether/sample"));
- Path sourceFile = sourceDir.resolve("SampleConfigurationKeys.java");
- try (InputStream in =
ConfigurationCollectorDocletTest.class.getResourceAsStream(FIXTURE)) {
- assertNotNull(in, "fixture source not found on classpath: " +
FIXTURE);
+ /**
+ * Classpath location of the a fixture with invalid javadoc
(missing/invalid elements).
+ */
+ private static final String INVALID_FIXTURE =
"/org/eclipse/aether/sample/InvalidSampleConfigurationKeys.java";
+
+ private Path output;
+
+ @BeforeEach
+ void setUp(@TempDir Path tempDir) {
+ output = tempDir.resolve("configuration-keys.properties");
+ }
+
+ private Path getSourceFile(String resourcePath, Path tempDir) throws
Exception {
+ if (!resourcePath.startsWith("/")) {
+ throw new IllegalArgumentException("resource path must start with
'/': " + resourcePath);
+ }
+ Path sourceDir =
+
Files.createDirectories(tempDir.resolve(resourcePath.substring(1,
resourcePath.lastIndexOf('/'))));
+ Path sourceFile =
sourceDir.resolve(resourcePath.substring(resourcePath.lastIndexOf('/') + 1));
+ try (InputStream in =
ConfigurationCollectorDocletTest.class.getResourceAsStream(resourcePath)) {
+ assertNotNull(in, "resource path not found on classpath: " +
resourcePath);
Files.copy(in, sourceFile);
}
- Path output = tempDir.resolve("configuration-keys.properties");
+ return sourceFile;
+ }
- runDoclet(sourceFile, output);
+ @Test
+ void extractsBooleanStringAndEnumConfigurations(@TempDir Path tempDir)
throws Exception {
+ StringWriter out = new StringWriter();
+ assertTrue(
+ runDoclet(out, getSourceFile(FIXTURE, tempDir), output),
"doclet run should succeed, output:\n" + out);
Map<String, Map<String, String>> keys = readKeys(output);
assertEquals(4, keys.size(), "expected four configuration keys");
@@ -96,17 +123,85 @@ class ConfigurationCollectorDocletTest {
assertEquals("No", enum2Key.get("supportRepoIdSuffix"));
}
- private static void runDoclet(Path sourceFile, Path output) throws
Exception {
+ static final class CapturingDiagnosticsListener<T extends JavaFileObject>
+ implements javax.tools.DiagnosticListener<T> {
+ private final javax.tools.Diagnostic.Kind threshold;
+ private final Collection<Diagnostic<? extends JavaFileObject>>
diagnostics = new ArrayList<>();
+
+ public CapturingDiagnosticsListener(javax.tools.Diagnostic.Kind
threshold) {
+ this.threshold = threshold;
+ }
+
+ @Override
+ public void report(javax.tools.Diagnostic<? extends T> diagnostic) {
+ if (diagnostic.getKind().compareTo(threshold) <= 0) {
+ diagnostics.add(diagnostic);
+ }
+ }
+
+ public Collection<Diagnostic<? extends JavaFileObject>>
getDiagnostics() {
+ return diagnostics;
+ }
+ }
+
+ @Test
+ void invalidMode() throws Exception {
+ CapturingDiagnosticsListener<JavaFileObject> listener =
+ new
CapturingDiagnosticsListener<>(javax.tools.Diagnostic.Kind.ERROR);
+ StringWriter out = new StringWriter();
+ assertFalse(runDoclet(out, getSourceFile(FIXTURE, output.getParent()),
output, "invalid-mode", listener));
+ // check that the diagnostics contain an error message about the
invalid mode
+ Diagnostic<? extends JavaFileObject> diagnostic =
+ listener.getDiagnostics().iterator().next();
+ assertEquals(javax.tools.Diagnostic.Kind.ERROR, diagnostic.getKind());
+ assertEquals("Unknown mode: invalid-mode",
diagnostic.getMessage(null));
+ assertEquals(1, listener.getDiagnostics().size(), "expected one error
diagnostic");
+ }
+
+ @Test
+ void invalidTaglets() throws Exception {
+ CapturingDiagnosticsListener<JavaFileObject> listener =
+ new
CapturingDiagnosticsListener<>(javax.tools.Diagnostic.Kind.ERROR);
+ StringWriter out = new StringWriter();
+ Path sourceFile = getSourceFile(INVALID_FIXTURE, output.getParent());
+ assertFalse(runDoclet(out, sourceFile, output, "resolver", listener));
+ // check that the diagnostics contain two error messages
+ Iterator<Diagnostic<? extends JavaFileObject>> iterator =
+ listener.getDiagnostics().iterator();
+ Diagnostic<? extends JavaFileObject> diagnostic = iterator.next();
+ assertEquals(javax.tools.Diagnostic.Kind.ERROR, diagnostic.getKind());
+ assertEquals("Missing content for @configurationType",
diagnostic.getMessage(null));
+ assertEquals(sourceFile.toString(), diagnostic.getSource().getName());
+ assertEquals(30, diagnostic.getLineNumber());
+ assertEquals(2, listener.getDiagnostics().size(), "expected two error
diagnostics");
+ diagnostic = iterator.next();
+ assertEquals(javax.tools.Diagnostic.Kind.ERROR, diagnostic.getKind());
+ assertEquals("No valid {@link ...} reference found in
@configurationType", diagnostic.getMessage(null));
+ assertEquals(sourceFile.toString(), diagnostic.getSource().getName());
+ assertEquals(46, diagnostic.getLineNumber());
+ }
+
+ private static Boolean runDoclet(Writer writer, Path sourceFile, Path
output) throws Exception {
+ return runDoclet(writer, sourceFile, output, null, null);
+ }
+
+ private static Boolean runDoclet(
+ Writer writer, Path sourceFile, Path output, String mode,
DiagnosticListener<JavaFileObject> listener)
+ throws Exception {
DocumentationTool documentationTool =
ToolProvider.getSystemDocumentationTool();
try (StandardJavaFileManager fileManager =
documentationTool.getStandardFileManager(null, null,
StandardCharsets.UTF_8)) {
Iterable<? extends JavaFileObject> units =
fileManager.getJavaFileObjectsFromFiles(List.of(sourceFile.toFile()));
- List<String> options = List.of("--output", output.toString(),
"-encoding", "UTF-8");
- StringWriter out = new StringWriter();
+ final List<String> options;
+ if (mode != null) {
+ options = List.of("--output", output.toString(), "--mode",
mode, "-encoding", "UTF-8");
+ } else {
+ options = List.of("--output", output.toString(), "-encoding",
"UTF-8");
+ }
DocumentationTool.DocumentationTask task =
documentationTool.getTask(
- out, fileManager, null,
ConfigurationCollectorDoclet.class, options, units);
- assertTrue(task.call(), "doclet run should succeed, output:\n" +
out);
+ writer, fileManager, listener,
ConfigurationCollectorDoclet.class, options, units);
+ return task.call();
}
}
diff --git
a/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/InvalidSampleConfigurationKeys.java
b/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/InvalidSampleConfigurationKeys.java
new file mode 100644
index 000000000..c01a3c295
--- /dev/null
+++
b/maven-resolver-tools/src/test/resources/org/eclipse/aether/sample/InvalidSampleConfigurationKeys.java
@@ -0,0 +1,55 @@
+/*
+ * 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.sample;
+
+
+/**
+ * Sample source declaring configuration keys of type {@link Boolean}, {@link
String} and a custom enum, using the same
+ * Javadoc block tags that {@code ConfigurationCollectorDoclet} extracts. Used
as a fixture by the doclet test.
+ */
+public final class InvalidSampleConfigurationKeys {
+
+ // missing mandatory @configurationType tag, should be reported as an
error by the doclet
+ /**
+ * A boolean flag.
+ *
+ * @since 1.2.3
+ * @configurationSource {@link System#getProperty(String,String)}
+ * @configurationDefaultValue {@link #DEFAULT_BOOL}
+ * @configurationRepoIdSuffix No
+ */
+ public static final String BOOL_KEY = "sample.bool";
+
+ public static final boolean DEFAULT_BOOL = true;
+
+ // invalid @configurationType tag value, should be reported as an error by
the doclet
+ /**
+ * A string value.
+ *
+ * @configurationSource {@link System#getProperty(String,String)}
+ * @configurationType invalid
+ * @configurationDefaultValue {@link #DEFAULT_STRING}
+ * @configurationRepoIdSuffix Yes
+ */
+ public static final String STRING_KEY = "sample.string";
+
+ public static final String DEFAULT_STRING = "hello";
+
+ private InvalidSampleConfigurationKeys() {}
+}