Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce documentation-support module #428

Merged
merged 51 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a1355c1
Introduce `documentation-generator` module
rickie Dec 23, 2022
1116991
Fix some mutants and improve code
rickie Dec 29, 2022
826f0a3
Two more mutants
rickie Dec 29, 2022
ae3a8e3
Was doing full build from wrong directory
rickie Dec 29, 2022
53fe306
Fixes to make build green for all JDKs
rickie Dec 29, 2022
304fa74
Make build Windows compatible
rickie Dec 29, 2022
5a0d7a2
Try new thing for Windows build
rickie Dec 29, 2022
1657c83
PSM-1716 Simplify path generation
nathankooij Dec 30, 2022
2323a08
PSM-1716 Split wrongPath test based on OS
nathankooij Dec 30, 2022
15453af
PSM-1716 Sort
nathankooij Dec 30, 2022
aa3b779
PSM-1716 Drop trailing character
nathankooij Dec 30, 2022
ce42b4d
PSM-1716 Suggestions
nathankooij Dec 30, 2022
03a2768
Apply suggestions and minor improvements
rickie Dec 31, 2022
783989f
Change `basePath` usage to not get Windows issue?
rickie Jan 2, 2023
e9143ee
Rename files and improve tests
rickie Jan 2, 2023
b0864d0
Optimize `DocGenTaskListener#started`
rickie Jan 2, 2023
f8b0261
Rename test and use correct `TaskEvent.Kind`
rickie Jan 2, 2023
4e03c9a
Major cleanup in the testing setup
rickie Jan 2, 2023
1fffc62
Further simplify compiler test setup
rickie Jan 2, 2023
d8ed827
PSM-1716 Suggestions
nathankooij Jan 3, 2023
344af56
PSM-1716 Forgot to change the message
nathankooij Jan 3, 2023
2fe7c30
PSM-1716 Fix violation
nathankooij Jan 3, 2023
2b94450
PSM-1716 Try `setWritable` for Windows instead
nathankooij Jan 3, 2023
d0fccf2
PSM-1716 Fix violation
nathankooij Jan 3, 2023
a36664a
PSM-1716 Windows
nathankooij Jan 3, 2023
fd847c9
PSM-1716 Do pass the parameter
nathankooij Jan 3, 2023
1967f1c
PSM-1716 Fix error message on Windows
nathankooij Jan 3, 2023
c43809d
`s/package tech.picnic.errorprone.{plugin -> documentation}`
rickie Jan 3, 2023
60f506e
Suggestions
rickie Jan 3, 2023
6cd66c1
Use `Splitter` API instead of `String#split`
rickie Jan 6, 2023
58e30e6
PSM-1716 Drop `documentation-generator` Maven profile
rickie Jan 6, 2023
69ed2ae
This is a works on my machine....
rickie Jan 6, 2023
fdf4204
Apply suggestion
rickie Jan 6, 2023
d3ecbc0
Try something else to fix build
rickie Jan 6, 2023
c4de095
`s/disableable/canDisable/`
rickie Jan 6, 2023
fe71a5d
Revert "This is a works on my machine...."
rickie Jan 6, 2023
c369106
Revert "PSM-1716 Drop `documentation-generator` Maven profile"
rickie Jan 6, 2023
401b31a
Dont update the images
rickie Jan 6, 2023
f3c8618
Further revert my mistake
rickie Jan 6, 2023
9a7720a
Version bump
Stephan202 Jan 15, 2023
357d736
Suggestions
Stephan202 Jan 15, 2023
d1204b1
Make `TaskListenerCompiler` a util
rickie Jan 23, 2023
5440cf1
Add dot and `static` modifier
rickie Jan 25, 2023
46faf0d
Configure `testFileManager` to output test files to
rickie Jan 25, 2023
5406bd3
Move tests and apply suggestions
rickie Jan 25, 2023
12b1008
Rename module and some classes
rickie Jan 25, 2023
52492ea
Bump version
Stephan202 Feb 4, 2023
b85f222
PSM-1716 Suggestions
Stephan202 Feb 4, 2023
9dfc173
PSM-1716 Rename enum
Stephan202 Feb 11, 2023
485c85c
Wanna test this...
Stephan202 Feb 11, 2023
2f8dd30
Suggestions
Stephan202 Feb 11, 2023
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
87 changes: 87 additions & 0 deletions documentation-support/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>tech.picnic.error-prone-support</groupId>
<artifactId>error-prone-support</artifactId>
<version>0.8.1-SNAPSHOT</version>
</parent>

<artifactId>documentation-support</artifactId>

<name>Picnic :: Error Prone Support :: Documentation Support</name>
<description>Data extraction support for the purpose of documentation generation.</description>

<dependencies>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotation</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_check_api</artifactId>
</dependency>
<dependency>
<groupId>${groupId.error-prone}</groupId>
<artifactId>error_prone_test_helpers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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.
*/
@AutoService(Plugin.class)
public final class DocumentationGenerator implements Plugin {
Copy link
Member

Choose a reason for hiding this comment

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

🤔 This class doesn't actually generate documentation. Let me chew on an alternative name 🤔

(Likewise for DocumentationGeneratorTaskListener.)

Copy link
Member

Choose a reason for hiding this comment

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

K, let's defer this; will add an XXX comment.

@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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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.
*/
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", "");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Loading