mirror of
https://github.com/jlengrand/error-prone-support.git
synced 2026-03-10 08:11:25 +00:00
Introduce documentation-support module (#428)
This new module provides the initial version of a framework for the extraction of data from bug checkers and Refaster rules, to be used as input for website generation.
This commit is contained in:
committed by
GitHub
parent
14b5fa1feb
commit
0f15070883
@@ -0,0 +1,103 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.google.auto.common.AnnotationMirrors;
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.BugPattern.SeverityLevel;
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.google.errorprone.util.ASTHelpers;
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.code.Attribute;
|
||||
import com.sun.tools.javac.code.Symbol.ClassSymbol;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import javax.lang.model.element.AnnotationValue;
|
||||
import tech.picnic.errorprone.documentation.BugPatternExtractor.BugPatternDocumentation;
|
||||
|
||||
/**
|
||||
* An {@link Extractor} that describes how to extract data from a {@code @BugPattern} annotation.
|
||||
*/
|
||||
@Immutable
|
||||
final class BugPatternExtractor implements Extractor<BugPatternDocumentation> {
|
||||
@Override
|
||||
public BugPatternDocumentation extract(ClassTree tree, Context context) {
|
||||
ClassSymbol symbol = ASTHelpers.getSymbol(tree);
|
||||
BugPattern annotation = symbol.getAnnotation(BugPattern.class);
|
||||
requireNonNull(annotation, "BugPattern annotation must be present");
|
||||
|
||||
return new AutoValue_BugPatternExtractor_BugPatternDocumentation(
|
||||
symbol.getQualifiedName().toString(),
|
||||
annotation.name().isEmpty() ? tree.getSimpleName().toString() : annotation.name(),
|
||||
ImmutableList.copyOf(annotation.altNames()),
|
||||
annotation.link(),
|
||||
ImmutableList.copyOf(annotation.tags()),
|
||||
annotation.summary(),
|
||||
annotation.explanation(),
|
||||
annotation.severity(),
|
||||
annotation.disableable(),
|
||||
annotation.documentSuppression() ? getSuppressionAnnotations(tree) : ImmutableList.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExtract(ClassTree tree) {
|
||||
return ASTHelpers.hasDirectAnnotationWithSimpleName(tree, BugPattern.class.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified class names of suppression annotations specified by the {@link
|
||||
* BugPattern} annotation located on the given tree.
|
||||
*
|
||||
* @implNote This method cannot simply invoke {@link BugPattern#suppressionAnnotations()}, as that
|
||||
* will yield an "Attempt to access Class objects for TypeMirrors" exception.
|
||||
*/
|
||||
private static ImmutableList<String> getSuppressionAnnotations(ClassTree tree) {
|
||||
AnnotationTree annotationTree =
|
||||
ASTHelpers.getAnnotationWithSimpleName(
|
||||
ASTHelpers.getAnnotations(tree), BugPattern.class.getSimpleName());
|
||||
requireNonNull(annotationTree, "BugPattern annotation must be present");
|
||||
|
||||
Attribute.Array types =
|
||||
doCast(
|
||||
AnnotationMirrors.getAnnotationValue(
|
||||
ASTHelpers.getAnnotationMirror(annotationTree), "suppressionAnnotations"),
|
||||
Attribute.Array.class);
|
||||
|
||||
return types.getValue().stream()
|
||||
.map(v -> doCast(v, Attribute.Class.class).classType.toString())
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends AnnotationValue> T doCast(AnnotationValue value, Class<T> target) {
|
||||
verify(target.isInstance(value), "Value '%s' is not of type '%s'", value, target);
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class BugPatternDocumentation {
|
||||
abstract String fullyQualifiedName();
|
||||
|
||||
abstract String name();
|
||||
|
||||
abstract ImmutableList<String> altNames();
|
||||
|
||||
abstract String link();
|
||||
|
||||
abstract ImmutableList<String> tags();
|
||||
|
||||
abstract String summary();
|
||||
|
||||
abstract String explanation();
|
||||
|
||||
abstract SeverityLevel severityLevel();
|
||||
|
||||
abstract boolean canDisable();
|
||||
|
||||
abstract ImmutableList<String> suppressionAnnotations();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.Plugin;
|
||||
import com.sun.tools.javac.api.BasicJavacTask;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A compiler {@link Plugin} that analyzes and extracts relevant information for documentation
|
||||
* purposes from processed files.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
@AutoService(Plugin.class)
|
||||
public final class DocumentationGenerator implements Plugin {
|
||||
@VisibleForTesting static final String OUTPUT_DIRECTORY_FLAG = "-XoutputDirectory";
|
||||
private static final Pattern OUTPUT_DIRECTORY_FLAG_PATTERN =
|
||||
Pattern.compile(Pattern.quote(OUTPUT_DIRECTORY_FLAG) + "=(.*)");
|
||||
|
||||
/** Instantiates a new {@link DocumentationGenerator} instance. */
|
||||
public DocumentationGenerator() {}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(JavacTask javacTask, String... args) {
|
||||
checkArgument(args.length == 1, "Precisely one path must be provided");
|
||||
|
||||
javacTask.addTaskListener(
|
||||
new DocumentationGeneratorTaskListener(
|
||||
((BasicJavacTask) javacTask).getContext(), getOutputPath(args[0])));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static Path getOutputPath(String pathArg) {
|
||||
Matcher matcher = OUTPUT_DIRECTORY_FLAG_PATTERN.matcher(pathArg);
|
||||
checkArgument(
|
||||
matcher.matches(), "'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
|
||||
String path = matcher.group(1);
|
||||
try {
|
||||
return Path.of(path);
|
||||
} catch (InvalidPathException e) {
|
||||
throw new IllegalArgumentException(String.format("Invalid path '%s'", path), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskEvent.Kind;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.tools.javac.api.JavacTrees;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
/**
|
||||
* A {@link TaskListener} that identifies and extracts relevant content for documentation generation
|
||||
* and writes it to disk.
|
||||
*/
|
||||
// XXX: Find a better name for this class; it doesn't generate documentation per se.
|
||||
final class DocumentationGeneratorTaskListener implements TaskListener {
|
||||
private static final ObjectMapper OBJECT_MAPPER =
|
||||
new ObjectMapper().setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
|
||||
|
||||
private final Context context;
|
||||
private final Path docsPath;
|
||||
|
||||
DocumentationGeneratorTaskListener(Context context, Path path) {
|
||||
this.context = context;
|
||||
this.docsPath = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void started(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() == Kind.ANALYZE) {
|
||||
createDocsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent taskEvent) {
|
||||
if (taskEvent.getKind() != Kind.ANALYZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClassTree classTree = JavacTrees.instance(context).getTree(taskEvent.getTypeElement());
|
||||
JavaFileObject sourceFile = taskEvent.getSourceFile();
|
||||
if (classTree == null || sourceFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ExtractorType.findMatchingType(classTree)
|
||||
.ifPresent(
|
||||
extractorType ->
|
||||
writeToFile(
|
||||
extractorType.getIdentifier(),
|
||||
getSimpleClassName(sourceFile.toUri()),
|
||||
extractorType.getExtractor().extract(classTree, context)));
|
||||
}
|
||||
|
||||
private void createDocsDirectory() {
|
||||
try {
|
||||
Files.createDirectories(docsPath);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Error while creating directory with path '%s'", docsPath), e);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void writeToFile(String identifier, String className, T data) {
|
||||
File file = docsPath.resolve(String.format("%s-%s.json", identifier, className)).toFile();
|
||||
|
||||
try (FileWriter fileWriter = new FileWriter(file, UTF_8)) {
|
||||
OBJECT_MAPPER.writeValue(fileWriter, data);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(String.format("Cannot write to file '%s'", file.getPath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSimpleClassName(URI path) {
|
||||
return Paths.get(path).getFileName().toString().replace(".java", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.errorprone.annotations.Immutable;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import com.sun.tools.javac.util.Context;
|
||||
|
||||
/**
|
||||
* Interface implemented by classes that define how to extract data of some type {@link T} from a
|
||||
* given {@link ClassTree}.
|
||||
*
|
||||
* @param <T> The type of data that is extracted.
|
||||
*/
|
||||
@Immutable
|
||||
interface Extractor<T> {
|
||||
/**
|
||||
* Extracts and returns an instance of {@link T} using the provided arguments.
|
||||
*
|
||||
* @param tree The {@link ClassTree} to analyze and from which to extract instances of {@link T}.
|
||||
* @param context The {@link Context} in which the current compilation takes place.
|
||||
* @return A non-null instance of {@link T}.
|
||||
*/
|
||||
// XXX: Drop `Context` parameter unless used.
|
||||
T extract(ClassTree tree, Context context);
|
||||
|
||||
/**
|
||||
* Tells whether this {@link Extractor} can extract documentation content from the given {@link
|
||||
* ClassTree}.
|
||||
*
|
||||
* @param tree The {@link ClassTree} of interest.
|
||||
* @return {@code true} iff data extraction is supported.
|
||||
*/
|
||||
boolean canExtract(ClassTree tree);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
/** An enumeration of {@link Extractor} types. */
|
||||
enum ExtractorType {
|
||||
BUG_PATTERN("bugpattern", new BugPatternExtractor());
|
||||
|
||||
private static final ImmutableSet<ExtractorType> TYPES =
|
||||
Sets.immutableEnumSet(EnumSet.allOf(ExtractorType.class));
|
||||
|
||||
private final String identifier;
|
||||
private final Extractor<?> extractor;
|
||||
|
||||
ExtractorType(String identifier, Extractor<?> extractor) {
|
||||
this.identifier = identifier;
|
||||
this.extractor = extractor;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
Extractor<?> getExtractor() {
|
||||
return extractor;
|
||||
}
|
||||
|
||||
static Optional<ExtractorType> findMatchingType(ClassTree tree) {
|
||||
return TYPES.stream().filter(type -> type.getExtractor().canExtract(tree)).findFirst();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* A Java compiler plugin that extracts data from compiled classes, in support of the Error Prone
|
||||
* Support documentation.
|
||||
*/
|
||||
@com.google.errorprone.annotations.CheckReturnValue
|
||||
@org.jspecify.annotations.NullMarked
|
||||
package tech.picnic.errorprone.documentation;
|
||||
@@ -0,0 +1,153 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import com.google.errorprone.BugPattern;
|
||||
import com.google.errorprone.CompilationTestHelper;
|
||||
import com.google.errorprone.VisitorState;
|
||||
import com.google.errorprone.bugpatterns.BugChecker;
|
||||
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
|
||||
import com.google.errorprone.matchers.Description;
|
||||
import com.sun.source.tree.ClassTree;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class BugPatternExtractorTest {
|
||||
@Test
|
||||
void noBugPattern(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"TestCheckerWithoutAnnotation.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"public final class TestCheckerWithoutAnnotation extends BugChecker {}");
|
||||
|
||||
assertThat(outputDirectory.toAbsolutePath()).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void minimalBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"MinimalBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(summary = \"MinimalBugChecker summary\", severity = SeverityLevel.ERROR)",
|
||||
"public final class MinimalBugChecker extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-MinimalBugChecker.json",
|
||||
"bugpattern-documentation-minimal.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"CompleteBugChecker.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"import org.junit.jupiter.api.Test;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" name = \"OtherName\",",
|
||||
" summary = \"CompleteBugChecker summary\",",
|
||||
" linkType = BugPattern.LinkType.CUSTOM,",
|
||||
" link = \"https://error-prone.picnic.tech\",",
|
||||
" explanation = \"Example explanation\",",
|
||||
" severity = SeverityLevel.SUGGESTION,",
|
||||
" altNames = \"Check\",",
|
||||
" tags = BugPattern.StandardTags.SIMPLIFICATION,",
|
||||
" disableable = false,",
|
||||
" suppressionAnnotations = {BugPattern.class, Test.class})",
|
||||
"public final class CompleteBugChecker extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-CompleteBugChecker.json",
|
||||
"bugpattern-documentation-complete.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void undocumentedSuppressionBugPattern(@TempDir Path outputDirectory) throws IOException {
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory,
|
||||
"UndocumentedSuppressionBugPattern.java",
|
||||
"package pkg;",
|
||||
"",
|
||||
"import com.google.errorprone.BugPattern;",
|
||||
"import com.google.errorprone.BugPattern.SeverityLevel;",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"@BugPattern(",
|
||||
" summary = \"UndocumentedSuppressionBugPattern summary\",",
|
||||
" severity = SeverityLevel.WARNING,",
|
||||
" documentSuppression = false)",
|
||||
"public final class UndocumentedSuppressionBugPattern extends BugChecker {}");
|
||||
|
||||
verifyFileMatchesResource(
|
||||
outputDirectory,
|
||||
"bugpattern-UndocumentedSuppressionBugPattern.json",
|
||||
"bugpattern-documentation-undocumented-suppression.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void bugPatternAnnotationIsAbsent() {
|
||||
CompilationTestHelper.newInstance(TestChecker.class, getClass())
|
||||
.addSourceLines(
|
||||
"TestChecker.java",
|
||||
"import com.google.errorprone.bugpatterns.BugChecker;",
|
||||
"",
|
||||
"// BUG: Diagnostic contains: Can extract: false",
|
||||
"public final class TestChecker extends BugChecker {}")
|
||||
.doTest();
|
||||
}
|
||||
|
||||
private static void verifyFileMatchesResource(
|
||||
Path outputDirectory, String fileName, String resourceName) throws IOException {
|
||||
assertThat(Files.readString(outputDirectory.resolve(fileName)))
|
||||
.isEqualToIgnoringWhitespace(getResource(resourceName));
|
||||
}
|
||||
|
||||
// XXX: Once we support only JDK 15+, drop this method in favour of including the resources as
|
||||
// text blocks in this class. (This also requires renaming the `verifyFileMatchesResource`
|
||||
// method.)
|
||||
private static String getResource(String resourceName) throws IOException {
|
||||
return Resources.toString(
|
||||
Resources.getResource(BugPatternExtractorTest.class, resourceName), UTF_8);
|
||||
}
|
||||
|
||||
/** A {@link BugChecker} that validates the {@link BugPatternExtractor}. */
|
||||
@BugPattern(summary = "Validates `BugPatternExtractor` extraction", severity = ERROR)
|
||||
public static final class TestChecker extends BugChecker implements ClassTreeMatcher {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Override
|
||||
public Description matchClass(ClassTree tree, VisitorState state) {
|
||||
BugPatternExtractor extractor = new BugPatternExtractor();
|
||||
|
||||
assertThatThrownBy(() -> extractor.extract(tree, state.context))
|
||||
.isInstanceOf(NullPointerException.class)
|
||||
.hasMessage("BugPattern annotation must be present");
|
||||
|
||||
return buildDescription(tree)
|
||||
.setMessage(String.format("Can extract: %s", extractor.canExtract(tree)))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.FileManagers;
|
||||
import com.google.errorprone.FileObjects;
|
||||
import com.sun.tools.javac.api.JavacTaskImpl;
|
||||
import com.sun.tools.javac.api.JavacTool;
|
||||
import com.sun.tools.javac.file.JavacFileManager;
|
||||
import java.nio.file.Path;
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
|
||||
// XXX: Generalize and move this class so that it can also be used by `refaster-compiler`.
|
||||
// XXX: Add support for this class to the `ErrorProneTestHelperSourceFormat` check.
|
||||
public final class Compilation {
|
||||
private Compilation() {}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
Path outputDirectory, String fileName, String... lines) {
|
||||
compileWithDocumentationGenerator(outputDirectory.toAbsolutePath().toString(), fileName, lines);
|
||||
}
|
||||
|
||||
public static void compileWithDocumentationGenerator(
|
||||
String outputDirectory, String fileName, String... lines) {
|
||||
compile(
|
||||
ImmutableList.of("-Xplugin:DocumentationGenerator -XoutputDirectory=" + outputDirectory),
|
||||
FileObjects.forSourceLines(fileName, lines));
|
||||
}
|
||||
|
||||
private static void compile(ImmutableList<String> options, JavaFileObject javaFileObject) {
|
||||
JavacFileManager javacFileManager = FileManagers.testFileManager();
|
||||
JavaCompiler compiler = JavacTool.create();
|
||||
JavacTaskImpl task =
|
||||
(JavacTaskImpl)
|
||||
compiler.getTask(
|
||||
null,
|
||||
javacFileManager,
|
||||
null,
|
||||
options,
|
||||
ImmutableList.of(),
|
||||
ImmutableList.of(javaFileObject));
|
||||
|
||||
task.call();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static java.nio.file.attribute.AclEntryPermission.ADD_SUBDIRECTORY;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.api.condition.OS.WINDOWS;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.AclEntry;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
final class DocumentationGeneratorTaskListenerTest {
|
||||
@EnabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemWindows(@TempDir Path outputDirectory) throws IOException {
|
||||
AclFileAttributeView view =
|
||||
Files.getFileAttributeView(outputDirectory, AclFileAttributeView.class);
|
||||
view.setAcl(
|
||||
view.getAcl().stream()
|
||||
.map(
|
||||
entry ->
|
||||
AclEntry.newBuilder(entry)
|
||||
.setPermissions(
|
||||
Sets.difference(entry.permissions(), ImmutableSet.of(ADD_SUBDIRECTORY)))
|
||||
.build())
|
||||
.collect(toImmutableList()));
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
@DisabledOnOs(WINDOWS)
|
||||
@Test
|
||||
void readOnlyFileSystemNonWindows(@TempDir Path outputDirectory) {
|
||||
assertThat(outputDirectory.toFile().setWritable(false))
|
||||
.describedAs("Failed to make test directory unwritable")
|
||||
.isTrue();
|
||||
|
||||
readOnlyFileSystemFailsToWrite(outputDirectory.resolve("nonexistent"));
|
||||
}
|
||||
|
||||
private static void readOnlyFileSystemFailsToWrite(Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory, "A.java", "class A {}"))
|
||||
.hasRootCauseInstanceOf(FileSystemException.class)
|
||||
.hasCauseInstanceOf(IllegalStateException.class)
|
||||
.hasMessageEndingWith("Error while creating directory with path '%s'", outputDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noClassNoOutput(@TempDir Path outputDirectory) {
|
||||
Compilation.compileWithDocumentationGenerator(outputDirectory, "A.java", "package pkg;");
|
||||
|
||||
assertThat(outputDirectory).isEmptyDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void excessArguments(@TempDir Path outputDirectory) {
|
||||
assertThatThrownBy(
|
||||
() ->
|
||||
Compilation.compileWithDocumentationGenerator(
|
||||
outputDirectory.toAbsolutePath() + " extra-arg", "A.java", "package pkg;"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("Precisely one path must be provided");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tech.picnic.errorprone.documentation;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static tech.picnic.errorprone.documentation.DocumentationGenerator.OUTPUT_DIRECTORY_FLAG;
|
||||
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
final class DocumentationGeneratorTest {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"bar", "foo"})
|
||||
void getOutputPath(String path) {
|
||||
assertThat(DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + path))
|
||||
.isEqualTo(Path.of(path));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "-XoutputDirectory", "invalidOption=Test", "nothing"})
|
||||
void getOutputPathWithInvalidArgument(String pathArg) {
|
||||
assertThatThrownBy(() -> DocumentationGenerator.getOutputPath(pathArg))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("'%s' must be of the form '%s=<value>'", pathArg, OUTPUT_DIRECTORY_FLAG);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOutputPathWithInvalidPath() {
|
||||
String basePath = "path-with-null-char-\0";
|
||||
assertThatThrownBy(
|
||||
() -> DocumentationGenerator.getOutputPath(OUTPUT_DIRECTORY_FLAG + '=' + basePath))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasCauseInstanceOf(InvalidPathException.class)
|
||||
.hasMessageEndingWith("Invalid path '%s'", basePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.CompleteBugChecker",
|
||||
"name": "OtherName",
|
||||
"altNames": [
|
||||
"Check"
|
||||
],
|
||||
"link": "https://error-prone.picnic.tech",
|
||||
"tags": [
|
||||
"Simplification"
|
||||
],
|
||||
"summary": "CompleteBugChecker summary",
|
||||
"explanation": "Example explanation",
|
||||
"severityLevel": "SUGGESTION",
|
||||
"canDisable": false,
|
||||
"suppressionAnnotations": [
|
||||
"com.google.errorprone.BugPattern",
|
||||
"org.junit.jupiter.api.Test"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.MinimalBugChecker",
|
||||
"name": "MinimalBugChecker",
|
||||
"altNames": [],
|
||||
"link": "",
|
||||
"tags": [],
|
||||
"summary": "MinimalBugChecker summary",
|
||||
"explanation": "",
|
||||
"severityLevel": "ERROR",
|
||||
"canDisable": true,
|
||||
"suppressionAnnotations": [
|
||||
"java.lang.SuppressWarnings"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"fullyQualifiedName": "pkg.UndocumentedSuppressionBugPattern",
|
||||
"name": "UndocumentedSuppressionBugPattern",
|
||||
"altNames": [],
|
||||
"link": "",
|
||||
"tags": [],
|
||||
"summary": "UndocumentedSuppressionBugPattern summary",
|
||||
"explanation": "",
|
||||
"severityLevel": "WARNING",
|
||||
"canDisable": true,
|
||||
"suppressionAnnotations": []
|
||||
}
|
||||
Reference in New Issue
Block a user