diff --git a/docgen/pom.xml b/docgen/pom.xml
new file mode 100644
index 00000000..cd1fb7df
--- /dev/null
+++ b/docgen/pom.xml
@@ -0,0 +1,162 @@
+
+
+
+
+ 4.0.0
+
+
+ tech.picnic.error-prone-support
+ error-prone-support
+ 0.2.1-SNAPSHOT
+
+
+ Picnic :: Error Prone Support :: DocGen
+ error_prone_docgen
+
+
+
+ Apache 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ com.google.auto.value
+ auto-value
+ ${version.auto-value}
+
+
+ com.google.auto.service
+ auto-service
+ ${version.auto-service}
+
+
+
+
+
+
+
+ src/main/java
+
+ **/*.mustache
+
+
+
+
+
+
+
+ ${groupId.error-prone}
+ error_prone_annotation
+
+
+ ${groupId.error-prone}
+ error_prone_core
+
+
+ ${project.groupId}
+ error-prone-contrib
+ ${project.version}
+
+
+ tech.picnic.error-prone-support
+ error_prone_docgen_processor
+ ${project.version}
+
+
+ com.google.guava
+ guava
+
+
+ org.yaml
+ snakeyaml
+ 1.30
+
+
+ junit
+ junit
+ test
+
+
+ com.beust
+ jcommander
+ 1.82
+
+
+ com.google.auto.value
+ auto-value-annotations
+ ${version.auto-value}
+ provided
+
+
+ com.github.spullara.mustache.java
+ compiler
+ 0.9.10
+
+
+ com.google.code.gson
+ gson
+ 2.8.9
+
+
+ com.google.truth
+ truth
+ test
+
+
+
+
+
+ run-annotation-processor
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ site
+
+ java
+
+
+ com.google.errorprone.DocGenTool
+
+
+ -bug_patterns=${basedir}/../error-prone-contrib/target/generated-sources/annotations/bugPatterns.txt
+
+ -docs_repository=${basedir}/target/generated-wiki/
+ -explanations=${basedir}/../docs/bugpattern-temp/
+
+
+
+
+
+
+
+
+
+
diff --git a/docgen/src/main/java/com/google/errorprone/BugPatternFileGenerator.java b/docgen/src/main/java/com/google/errorprone/BugPatternFileGenerator.java
new file mode 100644
index 00000000..322f0d51
--- /dev/null
+++ b/docgen/src/main/java/com/google/errorprone/BugPatternFileGenerator.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2014 The Error Prone Authors.
+ *
+ * Licensed 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 com.google.errorprone;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.LineProcessor;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.representer.Representer;
+
+/**
+ * Reads each line of the bugpatterns.txt tab-delimited data file, and generates a GitHub Jekyll
+ * page for each one.
+ *
+ * @author alexeagle@google.com (Alex Eagle)
+ */
+class BugPatternFileGenerator implements LineProcessor> {
+
+ private final Path outputDir;
+ private final Path explanationDir;
+ private final List result;
+
+ private final Function severityRemapper;
+
+ /** The base url for links to bugpatterns. */
+ @Nullable private final String baseUrl;
+
+ public BugPatternFileGenerator(
+ Path bugpatternDir,
+ Path explanationDir,
+ String baseUrl,
+ Function severityRemapper) {
+ this.outputDir = bugpatternDir;
+ this.explanationDir = explanationDir;
+ this.severityRemapper = severityRemapper;
+ this.baseUrl = baseUrl;
+ result = new ArrayList<>();
+ }
+
+ @Override
+ public boolean processLine(String line) throws IOException {
+ BugPatternInstance pattern = new Gson().fromJson(line, BugPatternInstance.class);
+ pattern.severity = severityRemapper.apply(pattern);
+ result.add(pattern);
+
+ // replace spaces in filename with underscores
+ Path checkPath = Paths.get(pattern.name.replace(' ', '_') + ".md");
+
+ try (Writer writer = Files.newBufferedWriter(outputDir.resolve(checkPath), UTF_8)) {
+
+ // load side-car explanation file, if it exists
+ Path sidecarExplanation = explanationDir.resolve(checkPath);
+ if (Files.exists(sidecarExplanation)) {
+ if (!pattern.explanation.isEmpty()) {
+ throw new AssertionError(
+ String.format(
+ "%s specifies an explanation via @BugPattern and side-car", pattern.name));
+ }
+ pattern.explanation = Files.readString(sidecarExplanation).trim();
+ }
+
+ // Construct an appropriate page for this {@code BugPattern}. Include altNames if
+ // there are any, and explain the correct way to suppress.
+
+ ImmutableMap.Builder templateData =
+ ImmutableMap.builder()
+ .put(
+ "tags", Arrays.stream(pattern.tags).map(Style::styleTag).collect(joining("\n\n")))
+ .put("severity", Style.styleSeverity(pattern.severity))
+ .put("name", pattern.name)
+ .put("bugpattern", String.format("%s.java", pattern.className))
+ .put("className", pattern.className)
+ .put("summary", pattern.summary.trim())
+ .put("altNames", Joiner.on(", ").join(pattern.altNames))
+ .put("explanation", pattern.explanation.trim());
+
+ if (pattern.sampleInput != null) {
+ templateData.put("sampleInput", pattern.sampleInput);
+ }
+
+ if (pattern.sampleOutput != null) {
+ templateData.put("sampleOutput", pattern.sampleOutput);
+ }
+
+ if (baseUrl != null) {
+ templateData.put("baseUrl", baseUrl);
+ }
+
+ if (pattern.documentSuppression) {
+ String suppressionString;
+ if (pattern.suppressionAnnotations.length == 0) {
+ suppressionString = "This check may not be suppressed.";
+ } else {
+ suppressionString =
+ pattern.suppressionAnnotations.length == 1
+ ? "Suppress false positives by adding the suppression annotation %s to the "
+ + "enclosing element."
+ : "Suppress false positives by adding one of these suppression annotations to "
+ + "the enclosing element: %s";
+ suppressionString =
+ String.format(
+ suppressionString,
+ Arrays.stream(pattern.suppressionAnnotations)
+ .map((String anno) -> standardizeAnnotation(anno, pattern.name))
+ .collect(joining(", ")));
+ }
+ templateData.put("suppression", suppressionString);
+ }
+
+ MustacheFactory mf = new DefaultMustacheFactory();
+ Mustache mustache = mf.compile("com/google/errorprone/resources/bugpattern.mustache");
+ mustache.execute(writer, templateData.buildOrThrow());
+ }
+ return true;
+ }
+
+ private String standardizeAnnotation(String fullAnnotationName, String patternName) {
+ String annotationName =
+ fullAnnotationName.endsWith(".class")
+ ? fullAnnotationName.substring(0, fullAnnotationName.length() - ".class".length())
+ : fullAnnotationName;
+ if (annotationName.equals(SuppressWarnings.class.getName())) {
+ annotationName = SuppressWarnings.class.getSimpleName() + "(\"" + patternName + "\")";
+ }
+ return "`@" + annotationName + "`";
+ }
+
+ @Override
+ public List getResult() {
+ return result;
+ }
+}
diff --git a/docgen/src/main/java/com/google/errorprone/DocGenTool.java b/docgen/src/main/java/com/google/errorprone/DocGenTool.java
new file mode 100644
index 00000000..371ab27f
--- /dev/null
+++ b/docgen/src/main/java/com/google/errorprone/DocGenTool.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2015 The Error Prone Authors.
+ *
+ * Licensed 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 com.google.errorprone;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.io.Files.asCharSource;
+import static com.google.errorprone.scanner.BuiltInCheckerSuppliers.ENABLED_ERRORS;
+import static com.google.errorprone.scanner.BuiltInCheckerSuppliers.ENABLED_WARNINGS;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.beust.jcommander.IStringConverter;
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.google.common.base.Ascii;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import java.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.stream.StreamSupport;
+
+/**
+ * Utility main which consumes the same tab-delimited text file and generates GitHub pages for the
+ * BugPatterns.
+ */
+public final class DocGenTool {
+
+ @Parameters(separators = "=")
+ static class Options {
+ @Parameter(names = "-bug_patterns", description = "Path to bugPatterns.txt", required = true)
+ private String bugPatterns;
+
+ @Parameter(
+ names = "-explanations",
+ description = "Path to side-car explanations",
+ required = true)
+ private String explanations;
+
+ @Parameter(names = "-docs_repository", description = "Path to docs repository", required = true)
+ private String docsRepository;
+
+ @Parameter(
+ names = "-base_url",
+ description = "The base url for links to bugpatterns",
+ arity = 1)
+ private String baseUrl = null;
+ }
+
+ public static void main(String[] args) throws IOException {
+ Options options = new Options();
+ new JCommander(options).parse(args);
+
+ Path bugPatterns = Paths.get(options.bugPatterns);
+ if (!Files.exists(bugPatterns)) {
+ usage("Cannot find bugPatterns file: " + options.bugPatterns);
+ }
+ Path explanationDir = Paths.get(options.explanations);
+ if (!Files.exists(explanationDir)) {
+ usage("Cannot find explanations dir: " + options.explanations);
+ }
+ Path wikiDir = Paths.get(options.docsRepository);
+ Files.createDirectories(wikiDir);
+ Path bugpatternDir = wikiDir.resolve("bugpatterns");
+ if (!Files.exists(bugpatternDir)) {
+ Files.createDirectories(bugpatternDir);
+ }
+ BugPatternFileGenerator generator =
+ new BugPatternFileGenerator(
+ bugpatternDir,
+ explanationDir,
+ options.baseUrl,
+ input -> input.severity);
+ try (Writer w =
+ Files.newBufferedWriter(wikiDir.resolve("bugpatterns.md"), StandardCharsets.UTF_8)) {
+ List patterns =
+ asCharSource(bugPatterns.toFile(), UTF_8).readLines(generator);
+ }
+ }
+
+ private static ImmutableSet enabledCheckNames() {
+ return StreamSupport.stream(
+ Iterables.concat(ENABLED_ERRORS, ENABLED_WARNINGS).spliterator(), false)
+ .map(BugCheckerInfo::canonicalName)
+ .collect(toImmutableSet());
+ }
+
+ private static void usage(String err) {
+ System.err.println(err);
+ System.exit(1);
+ }
+
+ private DocGenTool() {}
+}
diff --git a/docgen/src/main/java/com/google/errorprone/Style.java b/docgen/src/main/java/com/google/errorprone/Style.java
new file mode 100644
index 00000000..fdc79aa9
--- /dev/null
+++ b/docgen/src/main/java/com/google/errorprone/Style.java
@@ -0,0 +1,27 @@
+package com.google.errorprone;
+
+import com.google.errorprone.BugPattern.SeverityLevel;
+
+public final class Style {
+
+ public static String styleSeverity (SeverityLevel severityLevel) {
+ return String.format("%s\n {: .label .label-%s}", severityLevel.toString(), getSeverityLabelColour(severityLevel));
+ }
+
+ private static String getSeverityLabelColour (SeverityLevel severityLevel) {
+ switch (severityLevel) {
+ case ERROR:
+ return "red";
+ case WARNING:
+ return "yellow";
+ case SUGGESTION:
+ return "green";
+ default:
+ return "blue";
+ }
+ }
+
+ public static String styleTag (String tagName) {
+ return String.format("%s\n {: .label }", tagName);
+ }
+}
diff --git a/docgen/src/main/java/com/google/errorprone/resources/bugpattern.mustache b/docgen/src/main/java/com/google/errorprone/resources/bugpattern.mustache
new file mode 100644
index 00000000..dab54694
--- /dev/null
+++ b/docgen/src/main/java/com/google/errorprone/resources/bugpattern.mustache
@@ -0,0 +1,51 @@
+---
+layout: default
+title: {{name}}
+parent: Bug Patterns
+nav_order: 1
+---
+
+
+# {{name}}
+
+{{{severity}}}
+{{{tags}}}
+{{{summary}}}
+
+{{#suppression}}
+## Suppression
+{{{suppression}}}
+{{/suppression}}
+
+## Samples
+### Input
+```java
+{{#sampleInput}}
+{{{sampleInput}}}
+{{/sampleInput}}
+{{^sampleInput}}
+ public static void sample() {}
+{{/sampleInput}}
+```
+### Output
+```java
+{{#sampleOutput}}
+ {{{sampleOutput}}}
+{{/sampleOutput}}
+{{^sampleOutput}}
+ public static void sample() {}
+{{/sampleOutput}}
+```
+
+
+ View source code on GitHub
+
+
+
+
diff --git a/docgen/src/test/java/com/google/errorprone/BugPatternFileGeneratorTest.java b/docgen/src/test/java/com/google/errorprone/BugPatternFileGeneratorTest.java
new file mode 100644
index 00000000..e99ecdd2
--- /dev/null
+++ b/docgen/src/test/java/com/google/errorprone/BugPatternFileGeneratorTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2014 The Error Prone Authors.
+ *
+ * Licensed 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 com.google.errorprone;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.io.CharStreams;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.gson.Gson;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class BugPatternFileGeneratorTest {
+
+ @Rule public TemporaryFolder tmpfolder = new TemporaryFolder();
+
+ private Path wikiDir;
+ private Path explanationDirBase;
+
+ @Before
+ public void setUp() throws Exception {
+ wikiDir = tmpfolder.newFolder("wiki").toPath();
+ explanationDirBase = tmpfolder.newFolder("explanations").toPath();
+ }
+
+ private static BugPatternInstance deadExceptionTestInfo() {
+ BugPatternInstance instance = new BugPatternInstance();
+ instance.className = "com.google.errorprone.bugpatterns.DeadException";
+ instance.name = "DeadException";
+ instance.summary = "Exception created but not thrown";
+ instance.explanation =
+ "The exception is created with new, but is not thrown, and the reference is lost.";
+ instance.altNames = new String[] {"ThrowableInstanceNeverThrown"};
+ instance.tags = new String[] {"LikelyError"};
+ instance.severity = SeverityLevel.ERROR;
+ instance.suppressionAnnotations = new String[] {"java.lang.SuppressWarnings.class"};
+ return instance;
+ }
+
+ private static final String BUGPATTERN_LINE;
+
+ static {
+ BugPatternInstance instance = deadExceptionTestInfo();
+ BUGPATTERN_LINE = new Gson().toJson(instance);
+ }
+
+ private static final String BUGPATTERN_LINE_SIDECAR;
+
+ static {
+ BugPatternInstance instance = deadExceptionTestInfo();
+ instance.explanation = "";
+ BUGPATTERN_LINE_SIDECAR = new Gson().toJson(instance);
+ }
+
+ // Assert that the generator produces the same output it did before.
+ // This is brittle, but you can open the golden file
+ // src/test/resources/com/google/errorprone/DeadException.md
+ // in the same Jekyll environment you use for prod, and verify it looks good.
+ @Test
+ public void regressionTest_frontmatter_pygments() throws Exception {
+ BugPatternFileGenerator generator =
+ new BugPatternFileGenerator(
+ wikiDir, explanationDirBase, null, input -> input.severity);
+ generator.processLine(BUGPATTERN_LINE);
+ String expected =
+ CharStreams.toString(
+ new InputStreamReader(
+ getClass().getResourceAsStream("testdata/DeadException_frontmatter_pygments.md"),
+ UTF_8));
+ String actual =
+ CharStreams.toString(Files.newBufferedReader(wikiDir.resolve("DeadException.md"), UTF_8));
+ assertThat(actual.trim()).isEqualTo(expected.trim());
+ }
+
+ @Test
+ public void regressionTest_nofrontmatter_gfm() throws Exception {
+ BugPatternFileGenerator generator =
+ new BugPatternFileGenerator(
+ wikiDir, explanationDirBase, null, input -> input.severity);
+ generator.processLine(BUGPATTERN_LINE);
+ String expected =
+ CharStreams.toString(
+ new InputStreamReader(
+ getClass().getResourceAsStream("testdata/DeadException_nofrontmatter_gfm.md"),
+ UTF_8));
+ String actual = new String(Files.readAllBytes(wikiDir.resolve("DeadException.md")), UTF_8);
+ assertThat(actual.trim()).isEqualTo(expected.trim());
+ }
+
+ @Test
+ public void regressionTest_sidecar() throws Exception {
+ BugPatternFileGenerator generator =
+ new BugPatternFileGenerator(
+ wikiDir, explanationDirBase, null, input -> input.severity);
+ Files.write(
+ explanationDirBase.resolve("DeadException.md"),
+ Arrays.asList(
+ "The exception is created with new, but is not thrown, and the reference is lost."),
+ UTF_8);
+ generator.processLine(BUGPATTERN_LINE_SIDECAR);
+ String expected =
+ CharStreams.toString(
+ new InputStreamReader(
+ getClass().getResourceAsStream("testdata/DeadException_nofrontmatter_gfm.md"),
+ UTF_8));
+ String actual = new String(Files.readAllBytes(wikiDir.resolve("DeadException.md")), UTF_8);
+ assertThat(actual.trim()).isEqualTo(expected.trim());
+ }
+
+ @Test
+ public void testEscapeAngleBracketsInSummary() throws Exception {
+ // Create a BugPattern with angle brackets in the summary
+ BugPatternInstance instance = new BugPatternInstance();
+ instance.className = "com.google.errorprone.bugpatterns.DontDoThis";
+ instance.name = "DontDoThis";
+ instance.summary = "Don't do this; do List instead";
+ instance.explanation = "This is a bad idea, you want `List` instead";
+ instance.altNames = new String[0];
+ instance.tags = new String[] {"LikelyError"};
+ instance.severity = SeverityLevel.ERROR;
+ instance.suppressionAnnotations = new String[] {"java.lang.SuppressWarnings.class"};
+
+ // Write markdown file
+ BugPatternFileGenerator generator =
+ new BugPatternFileGenerator(
+ wikiDir, explanationDirBase, null, input -> input.severity);
+ generator.processLine(new Gson().toJson(instance));
+ String expected =
+ CharStreams.toString(
+ new InputStreamReader(
+ getClass().getResourceAsStream("testdata/DontDoThis_nofrontmatter_gfm.md"), UTF_8));
+ String actual = new String(Files.readAllBytes(wikiDir.resolve("DontDoThis.md")), UTF_8);
+ assertThat(actual.trim()).isEqualTo(expected.trim());
+ }
+}
diff --git a/docgen/src/test/java/com/google/errorprone/testdata/DeadException_frontmatter_pygments.md b/docgen/src/test/java/com/google/errorprone/testdata/DeadException_frontmatter_pygments.md
new file mode 100644
index 00000000..09bca0e2
--- /dev/null
+++ b/docgen/src/test/java/com/google/errorprone/testdata/DeadException_frontmatter_pygments.md
@@ -0,0 +1,20 @@
+---
+title: DeadException
+summary: Exception created but not thrown
+layout: bugpattern
+tags: LikelyError
+severity: ERROR
+---
+
+
+
+_Alternate names: ThrowableInstanceNeverThrown_
+
+## The problem
+The exception is created with new, but is not thrown, and the reference is lost.
+
+## Suppression
+Suppress false positives by adding the suppression annotation `@SuppressWarnings("DeadException")` to the enclosing element.
diff --git a/docgen/src/test/java/com/google/errorprone/testdata/DeadException_nofrontmatter_gfm.md b/docgen/src/test/java/com/google/errorprone/testdata/DeadException_nofrontmatter_gfm.md
new file mode 100644
index 00000000..1322b28a
--- /dev/null
+++ b/docgen/src/test/java/com/google/errorprone/testdata/DeadException_nofrontmatter_gfm.md
@@ -0,0 +1,21 @@
+
+
+# DeadException
+
+__Exception created but not thrown__
+
+
+
Severity
ERROR
+
Tags
LikelyError
+
+
+_Alternate names: ThrowableInstanceNeverThrown_
+
+## The problem
+The exception is created with new, but is not thrown, and the reference is lost.
+
+## Suppression
+Suppress false positives by adding the suppression annotation `@SuppressWarnings("DeadException")` to the enclosing element.
diff --git a/docgen/src/test/java/com/google/errorprone/testdata/DontDoThis_nofrontmatter_gfm.md b/docgen/src/test/java/com/google/errorprone/testdata/DontDoThis_nofrontmatter_gfm.md
new file mode 100644
index 00000000..25c4a439
--- /dev/null
+++ b/docgen/src/test/java/com/google/errorprone/testdata/DontDoThis_nofrontmatter_gfm.md
@@ -0,0 +1,20 @@
+
+
+# DontDoThis
+
+__Don't do this; do List<Foo> instead__
+
+
+
Severity
ERROR
+
Tags
LikelyError
+
+
+
+## The problem
+This is a bad idea, you want `List` instead
+
+## Suppression
+Suppress false positives by adding the suppression annotation `@SuppressWarnings("DontDoThis")` to the enclosing element.
diff --git a/docgen_processor/pom.xml b/docgen_processor/pom.xml
new file mode 100644
index 00000000..30e006f5
--- /dev/null
+++ b/docgen_processor/pom.xml
@@ -0,0 +1,89 @@
+
+
+
+
+ 4.0.0
+
+
+ tech.picnic.error-prone-support
+ error-prone-support
+ 0.2.1-SNAPSHOT
+
+
+ Picnic :: Error Prone Support :: DocGen Processor
+ error_prone_docgen_processor
+
+
+
+ Apache 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+ ${groupId.error-prone}
+ error_prone_annotation
+
+
+ ${groupId.error-prone}
+ error_prone_core
+
+
+ com.google.guava
+ guava
+
+
+ com.google.auto.service
+ auto-service-annotations
+ ${version.auto-service}
+
+
+ com.google.code.gson
+ gson
+ 2.8.9
+
+
+ com.google.googlejavaformat
+ google-java-format
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ com.google.auto.service
+ auto-service
+ ${version.auto-service}
+
+
+
+
+
+
+
+
diff --git a/docgen_processor/src/main/java/com/google/errorprone/BugPatternInstance.java b/docgen_processor/src/main/java/com/google/errorprone/BugPatternInstance.java
new file mode 100644
index 00000000..3077d69f
--- /dev/null
+++ b/docgen_processor/src/main/java/com/google/errorprone/BugPatternInstance.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2015 The Error Prone Authors.
+ *
+ * Licensed 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 com.google.errorprone;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Preconditions;
+import com.google.errorprone.BugPattern.SeverityLevel;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import org.eclipse.jgit.util.IO;
+
+/** A serialization-friendly POJO of the information in a {@link BugPattern}. */
+public final class BugPatternInstance {
+ private static final Formatter FORMATTER = new Formatter();
+
+ public String className;
+ public String name;
+ public String summary;
+ public String explanation;
+ public String[] altNames;
+ public String category;
+ public String[] tags;
+ public SeverityLevel severity;
+ public String[] suppressionAnnotations;
+ public boolean documentSuppression = true;
+
+ public String testContent;
+ public String sampleInput;
+ public String sampleOutput;
+
+ public static BugPatternInstance fromElement(Element element) {
+ BugPatternInstance instance = new BugPatternInstance();
+ instance.className = element.toString();
+
+ BugPattern annotation = element.getAnnotation(BugPattern.class);
+ instance.name =
+ annotation.name().isEmpty() ? element.getSimpleName().toString() : annotation.name();
+ instance.altNames = annotation.altNames();
+ instance.tags = annotation.tags();
+ instance.severity = annotation.severity();
+ instance.summary = annotation.summary();
+ instance.explanation = annotation.explanation();
+ instance.documentSuppression = annotation.documentSuppression();
+
+ Map keyValues = getAnnotation(element, BugPattern.class.getName());
+ Object suppression = keyValues.get("suppressionAnnotations");
+ if (suppression == null) {
+ instance.suppressionAnnotations = new String[] {SuppressWarnings.class.getName()};
+ } else {
+ Preconditions.checkState(suppression instanceof List);
+ @SuppressWarnings("unchecked") // Always List extends AnnotationValue>, see above.
+ List extends AnnotationValue> resultList = (List extends AnnotationValue>) suppression;
+ instance.suppressionAnnotations =
+ resultList.stream().map(AnnotationValue::toString).toArray(String[]::new);
+ }
+
+ Path testPath =
+ Path.of(
+ "error-prone-contrib/src/test/java/"
+ + instance.className.replace(".", "/")
+ + "Test.java");
+ System.out.println("test class for " + instance.name + " = " + testPath.toAbsolutePath());
+
+ try {
+ Pattern inputPattern =
+ Pattern.compile("\\.addInputLines\\((\\n.*?\".*?\",)\\n(.*?)\\)\\n", Pattern.DOTALL);
+ instance.testContent = String.join("\n", Files.readAllLines(testPath));
+ Matcher inputMatch = inputPattern.matcher(instance.testContent);
+ if (inputMatch.find()) {
+ String inputSrc =
+ inputMatch
+ .group(2) + ",\n";
+ System.out.println(inputSrc);
+ inputSrc = inputSrc.replaceAll("\\s*\"(.*?)\"(,\\n)", "$1\n");
+ System.out.println(inputSrc);
+ inputSrc = inputSrc
+ .replaceAll("\\\\\"(.*?)\\\\\"", "\"$1\"");
+ System.out.println(inputSrc);
+ instance.sampleInput = FORMATTER.formatSource(inputSrc);
+ }
+ } catch (IOException | IllegalStateException | FormatterException e) {
+ e.printStackTrace();
+ }
+
+ return instance;
+ }
+
+ private static Map getAnnotation(Element element, String name) {
+ for (AnnotationMirror mirror : element.getAnnotationMirrors()) {
+ if (mirror.getAnnotationType().toString().equals(name)) {
+ return annotationKeyValues(mirror);
+ }
+ }
+ throw new IllegalArgumentException(String.format("%s has no annotation %s", element, name));
+ }
+
+ private static Map annotationKeyValues(AnnotationMirror mirror) {
+ Map result = new LinkedHashMap<>();
+ for (ExecutableElement key : mirror.getElementValues().keySet()) {
+ result.put(key.getSimpleName().toString(), mirror.getElementValues().get(key).getValue());
+ }
+ return result;
+ }
+}
diff --git a/docgen_processor/src/main/java/com/google/errorprone/DocGenProcessor.java b/docgen_processor/src/main/java/com/google/errorprone/DocGenProcessor.java
new file mode 100644
index 00000000..7591e108
--- /dev/null
+++ b/docgen_processor/src/main/java/com/google/errorprone/DocGenProcessor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2011 The Error Prone Authors.
+ *
+ * Licensed 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 com.google.errorprone;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.auto.service.AutoService;
+import com.google.googlejavaformat.java.Formatter;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Set;
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+/**
+ * Annotation processor which visits all classes that have a {@code BugPattern} annotation, and
+ * writes a tab-delimited text file dumping the data found.
+ *
+ * @author eaftan@google.com (Eddie Aftandilian)
+ * @author alexeagle@google.com (Alex Eagle)
+ */
+@AutoService(Processor.class)
+@SupportedAnnotationTypes("com.google.errorprone.BugPattern")
+public class DocGenProcessor extends AbstractProcessor {
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ private final Gson gson = new Gson();
+
+ private PrintWriter pw;
+
+ /** {@inheritDoc} */
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ try {
+ FileObject manifest =
+ processingEnv
+ .getFiler()
+ .createResource(StandardLocation.SOURCE_OUTPUT, "", "bugPatterns.txt");
+ pw = new PrintWriter(new OutputStreamWriter(manifest.openOutputStream(), UTF_8), true);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ for (Element element : roundEnv.getElementsAnnotatedWith(BugPattern.class)) {
+ System.out.println("[DOCGEN] HANDLING: " + element.getSimpleName());
+ gson.toJson(BugPatternInstance.fromElement(element), pw);
+ pw.println();
+ }
+
+ if (roundEnv.processingOver()) {
+ // this was the last round, do cleanup
+ cleanup();
+ }
+ return false;
+ }
+
+ /** Perform cleanup after last round of annotation processing. */
+ private void cleanup() {
+ pw.close();
+ }
+}
diff --git a/docgen_processor/src/test/java/tech/picnic/errorprone/docgen/ExampleExtractorTest.java b/docgen_processor/src/test/java/tech/picnic/errorprone/docgen/ExampleExtractorTest.java
new file mode 100644
index 00000000..7ef7c472
--- /dev/null
+++ b/docgen_processor/src/test/java/tech/picnic/errorprone/docgen/ExampleExtractorTest.java
@@ -0,0 +1,90 @@
+package tech.picnic.errorprone.docgen;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.googlejavaformat.java.Formatter;
+import com.google.googlejavaformat.java.FormatterException;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+
+class ExampleExtractorTest {
+
+ private static final String INPUT =
+ String.join("\n",
+ "@Test",
+ "void replacementFirstSuggestedFix() {",
+ " refactoringTestHelper",
+ " .addInputLines(",
+ " \"A.java\",",
+ " \"import static java.util.stream.Collectors.toList;\",",
+ " \"import static java.util.stream.Collectors.toMap;\",",
+ " \"import static java.util.stream.Collectors.toSet;\",",
+ " \"\",",
+ " \"import java.util.stream.Collectors;\",",
+ " \"import java.util.stream.Stream;\",",
+ " \"import reactor.core.publisher.Flux;\",",
+ " \"\",",
+ " \"class A {\",",
+ " \" void m() {\",",
+ " \" Flux.just(1).collect(Collectors.toList());\",",
+ " \" Flux.just(2).collect(toList());\",",
+ " \"\",",
+ " \" Stream.of(\"foo\").collect(Collectors.toMap(String::getBytes, String::length));\",",
+ " \" Stream.of(\"bar\").collect(toMap(String::getBytes, String::length));\",",
+ " \" Flux.just(\"baz\").collect(Collectors.toMap(String::getBytes, String::length, (a, b) -> b));\",",
+ " \" Flux.just(\"qux\").collect(toMap(String::getBytes, String::length, (a, b) -> b));\",",
+ " \"\",",
+ " \" Stream.of(1).collect(Collectors.toSet());\",",
+ " \" Stream.of(2).collect(toSet());\",",
+ " \" }\",",
+ " \"}\")",
+ " .addOutputLines(",
+ " \"A.java\",",
+ " \"import static com.google.common.collect.ImmutableList.toImmutableList;\",",
+ " \"import static com.google.common.collect.ImmutableMap.toImmutableMap;\",",
+ " \"import static com.google.common.collect.ImmutableSet.toImmutableSet;\",",
+ " \"import static java.util.stream.Collectors.toList;\",",
+ " \"import static java.util.stream.Collectors.toMap;\",",
+ " \"import static java.util.stream.Collectors.toSet;\",",
+ " \"\",",
+ " \"import java.util.stream.Collectors;\",",
+ " \"import java.util.stream.Stream;\",",
+ " \"import reactor.core.publisher.Flux;\",",
+ " \"\",",
+ " \"class A {\",",
+ " \" void m() {\",",
+ " \" Flux.just(1).collect(toImmutableList());\",",
+ " \" Flux.just(2).collect(toImmutableList());\",",
+ " \"\",",
+ " \" Stream.of(\"foo\").collect(toImmutableMap(String::getBytes, String::length));\",",
+ " \" Stream.of(\"bar\").collect(toImmutableMap(String::getBytes, String::length));\",",
+ " \" Flux.just(\"baz\").collect(toImmutableMap(String::getBytes, String::length, (a, b) -> b));\",",
+ " \" Flux.just(\"qux\").collect(toImmutableMap(String::getBytes, String::length, (a, b) -> b));\",",
+ " \"\",",
+ " \" Stream.of(1).collect(toImmutableSet());\",",
+ " \" Stream.of(2).collect(toImmutableSet());\",",
+ " \" }\",",
+ " \"}\")",
+ " .doTest(TestMode.TEXT_MATCH);",
+ "}");
+
+ @Test
+ void regexTest() throws FormatterException {
+ final Formatter FORMATTER = new Formatter();
+ Pattern pattern =
+ Pattern.compile("\\.addInputLines\\((\n.*?\".*?\",)\n(.*?)\\)\n", Pattern.DOTALL);
+ Matcher matcher = pattern.matcher(INPUT);
+ int count = matcher.groupCount();
+ if(!matcher.find()) {
+ System.out.println("no match!");
+ return;
+ }
+
+ String src = matcher.group(2);
+ System.out.println("\\\"foo\\\"".replaceAll("\\\\\"(.*?)\\\\\"", "\"$1\""));
+ }
+}
diff --git a/docs/bugpatterns/AmbiguousJsonCreator.md b/docs/bugpatterns/AmbiguousJsonCreator.md
new file mode 100644
index 00000000..eec7c72a
--- /dev/null
+++ b/docs/bugpatterns/AmbiguousJsonCreator.md
@@ -0,0 +1,42 @@
+---
+layout: default
+title: AmbiguousJsonCreator
+parent: Bug Patterns
+nav_order: 1
+---
+
+
+# AmbiguousJsonCreator
+
+LikelyError
+
+${EXTRA_DOCS}
+
+## Samples
+
+\`\`\`java
+public static void sample() {}
+\`\`\`
+
+
+ View source code on GitHub
+
+
+
+
+# AmbiguousJsonCreator
+
+__`JsonCreator.Mode` should be set for single-argument creators__
+
+