Skip to content
Merged
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
101 changes: 101 additions & 0 deletions grader/src/it/java/edu/pdx/cs/joy/grader/APIDocumentationDocletIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package edu.pdx.cs.joy.grader;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import javax.tools.DocumentationTool;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;

public class APIDocumentationDocletIT {

@Test
void enumSyntheticMethodsAreNotDocumented(@TempDir Path tempDir) throws IOException {
String output = runDocletAgainst(tempDir, "Color.java", """
package edu.pdx.cs.joy.grader.fixtures;

/** Docs for Color. */
public enum Color {
RED;

/** Explicit helper. */
public String label() {
return name();
}
}
""");

assertThat(output, containsString("enum edu.pdx.cs.joy.grader.fixtures.Color"));
assertThat(output, containsString("label()"));
assertThat(output, not(containsString("Color()")));
assertThat(output, not(containsString("values()")));
assertThat(output, not(containsString("valueOf(String name)")));
}

@Test
void recordSyntheticAccessorIsNotDocumented(@TempDir Path tempDir) throws IOException {
String output = runDocletAgainst(tempDir, "Measurement.java", """
package edu.pdx.cs.joy.grader.fixtures;

/** Docs for Measurement. */
public record Measurement(int value) {

/** Explicit helper. */
public int doubled() {
return this.value * 2;
}
}
""");

assertThat(output, containsString("record edu.pdx.cs.joy.grader.fixtures.Measurement"));
assertThat(output, containsString("doubled()"));
assertThat(output, not(containsString("Measurement(int value)")));
assertThat(output, not(containsString("toString()")));
assertThat(output, not(containsString("hashCode()")));
assertThat(output, not(containsString("equals(Object o)")));
assertThat(output, not(containsString("value()")));
}

private String runDocletAgainst(Path tempDir, String fileName, String sourceCode) throws IOException {
Path packageDir = tempDir.resolve(Path.of("edu", "pdx", "cs", "joy", "grader", "fixtures"));
Files.createDirectories(packageDir);

Path sourceFile = packageDir.resolve(fileName);
Files.writeString(sourceFile, sourceCode);

DocumentationTool javadoc = ToolProvider.getSystemDocumentationTool();
assertThat("Javadoc tool should be available", javadoc, notNullValue());

ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintStream originalOut = System.out;

try (StandardJavaFileManager fileManager = javadoc.getStandardFileManager(null, null, null)) {
Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjects(sourceFile.toFile());
DocumentationTool.DocumentationTask task =
javadoc.getTask(new StringWriter(), fileManager, null, APIDocumentationDoclet.class, List.of("-quiet"), sources);

System.setOut(new PrintStream(out, true, StandardCharsets.UTF_8));
boolean succeeded = task.call();
assertThat("Javadoc task should succeed", succeeded, equalTo(true));
return out.toString(StandardCharsets.UTF_8);

} finally {
System.setOut(originalOut);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import org.jspecify.annotations.NonNull;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import java.io.PrintWriter;
import java.text.BreakIterator;
import java.util.*;
Expand All @@ -40,6 +42,15 @@ static String removePackageNames(String name) {
return name.replaceAll("(\\w+\\.)*(\\w+)", "$2");
}

@VisibleForTesting
static String classHeader(ElementKind kind, String qualifiedName) {
return switch (kind) {
case ENUM -> "enum " + qualifiedName;
case RECORD -> "record " + qualifiedName;
default -> "class " + qualifiedName;
};
}

@Override
public void init(Locale locale, Reporter reporter) {
this.reporter = reporter;
Expand Down Expand Up @@ -149,29 +160,51 @@ public boolean run(DocletEnvironment environment) {

DocTrees docTrees = environment.getDocTrees();
for (TypeElement aClass : ElementFilter.typesIn(environment.getIncludedElements())) {
generateClassDocumentation(docTrees, aClass, pw);
generateClassDocumentation(environment.getElementUtils(), docTrees, aClass, pw);
}
return true;
}

private void generateClassDocumentation(DocTrees docTrees, TypeElement aClass, PrintWriter pw) {
pw.println("Class " + aClass.getQualifiedName());
private void generateClassDocumentation(Elements elements, DocTrees docTrees, TypeElement aClass, PrintWriter pw) {
pw.println(classHeader(aClass.getKind(), aClass.getQualifiedName().toString()));

indent(getFullBodyComment(docTrees, aClass), 2, pw);
pw.println("");

Stream<? extends Element> constructors = aClass.getEnclosedElements().stream().filter(e -> e.getKind() == ElementKind.CONSTRUCTOR);
Stream<? extends Element> constructors = getConstructors(elements, docTrees, aClass);
constructors.forEach(constructor -> {
generateMethodDocumentation(docTrees, (ExecutableElement) constructor, pw);
});

Stream<? extends Element> methods = aClass.getEnclosedElements().stream().filter(e -> e.getKind() == ElementKind.METHOD);
Stream<? extends Element> methods = getMethods(elements, docTrees, aClass);
methods.forEach(method -> {
generateMethodDocumentation(docTrees, (ExecutableElement) method, pw);
});

}

private @NonNull Stream<? extends Element> getMethods(Elements elements, DocTrees docTrees, TypeElement aClass) {
return getElementsOfKind(elements, docTrees, aClass, ElementKind.METHOD);
}

private @NonNull Stream<? extends Element> getElementsOfKind(Elements elements, DocTrees docTrees, TypeElement aClass, ElementKind kind) {
return aClass.getEnclosedElements().stream()
.filter(e -> e.getKind() == kind)
.filter(e -> shouldDocument(elements, docTrees, (ExecutableElement) e));
}

private @NonNull Stream<? extends Element> getConstructors(Elements elements, DocTrees docTrees, TypeElement aClass) {
return getElementsOfKind(elements, docTrees, aClass, ElementKind.CONSTRUCTOR);
}

private boolean shouldDocument(Elements elements, DocTrees docTrees, ExecutableElement executable) {
if (executable.getKind() == ElementKind.CONSTRUCTOR) {
return elements.getOrigin(executable) == Elements.Origin.EXPLICIT;
}

return docTrees.getPath(executable) != null;
}

private void generateMethodDocumentation(DocTrees docTrees, ExecutableElement method, PrintWriter pw) {
StringBuilder sb = new StringBuilder();
sb.append(joinObjectsToStrings(method.getModifiers(), " ")).append(" ");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.junit.jupiter.api.Test;

import javax.lang.model.element.ElementKind;
import java.io.PrintWriter;
import java.io.StringWriter;

Expand All @@ -27,6 +28,24 @@ public void removePackageNameFromMapOfStringToString() {
assertThat(APIDocumentationDoclet.removePackageNames(fullName), equalTo(shortName));
}

@Test
public void classHeaderUsesLowerCaseClass() {
assertThat(APIDocumentationDoclet.classHeader(ElementKind.CLASS, "edu.pdx.cs.joy.grader.Foo"),
equalTo("class edu.pdx.cs.joy.grader.Foo"));
}

@Test
public void classHeaderUsesEnumForEnums() {
assertThat(APIDocumentationDoclet.classHeader(ElementKind.ENUM, "edu.pdx.cs.joy.grader.Color"),
equalTo("enum edu.pdx.cs.joy.grader.Color"));
}

@Test
public void classHeaderUsesRecordForRecords() {
assertThat(APIDocumentationDoclet.classHeader(ElementKind.RECORD, "edu.pdx.cs.joy.grader.Measurement"),
equalTo("record edu.pdx.cs.joy.grader.Measurement"));
}

@Test
public void leadingWhitespaceIsRemovedWhenIndenting() {
String rawText = "classes - The names of the classes the student is taking. A student \n" +
Expand Down
Loading