/* * Copyright 2014 Google Inc. All Rights Reserved. * * 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.base.Predicates.not; import static java.nio.charset.StandardCharsets.UTF_8; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.Mustache; import com.github.mustachejava.MustacheFactory; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.io.LineProcessor; import com.google.gson.Gson; import java.io.IOError; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; 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.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; /** * 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<List<BugPatternInstance>> { private static final Joiner COMMA_JOINER = Joiner.on(", "); private static final Function<String, String> ANNOTATE_AND_CODIFY = new Function<String, String>() { @Override public String apply(String annotationName) { Preconditions.checkState(annotationName.endsWith(".class")); return "`@" + annotationName.substring(0, annotationName.length() - ".class".length()) + "`"; } }; private final Path outputDir; private final Path exampleDirBase; private final Path explanationDir; private List<BugPatternInstance> result; /** * Enables pygments-style code highlighting blocks instead of github flavoured markdown style * code fences, because the latter make jekyll unhappy. */ private final boolean usePygments; /** * Controls whether yaml front-matter is generated. */ private final boolean generateFrontMatter; /** The base url for links to bugpatterns. */ @Nullable private final String baseUrl; public BugPatternFileGenerator( Path bugpatternDir, Path exampleDirBase, Path explanationDir, boolean generateFrontMatter, boolean usePygments, String baseUrl) { this.outputDir = bugpatternDir; this.exampleDirBase = exampleDirBase; this.explanationDir = explanationDir; this.generateFrontMatter = generateFrontMatter; this.usePygments = usePygments; this.baseUrl = baseUrl; result = new ArrayList<>(); } private static class ExampleFilter implements DirectoryStream.Filter<Path> { private Pattern matchPattern; public ExampleFilter(String checkerName) { this.matchPattern = Pattern.compile(checkerName + "(Positive|Negative)Case.*"); } @Override public boolean accept(Path entry) throws IOException { return Files.isDirectory(entry) || matchPattern.matcher(entry.getFileName().toString()).matches(); } } /** * A function to convert a test case file into an {@link ExampleInfo}. */ private static class PathToExampleInfo implements Function<Path, ExampleInfo> { private final String checkerClass; public PathToExampleInfo(String checkerClass) { this.checkerClass = checkerClass; } @Override public ExampleInfo apply(Path path) { ExampleInfo.ExampleKind posOrNeg = null; String fileName = path.getFileName().toString(); if (fileName.contains("Positive")) { posOrNeg = ExampleInfo.ExampleKind.POSITIVE; } else if (fileName.contains("Negative")) { posOrNeg = ExampleInfo.ExampleKind.NEGATIVE; } else { // ExampleFilter enforces this throw new AssertionError( "Example filename must contain \"Positive\" or \"Negative\", but was " + fileName); } String code; try { code = new String(Files.readAllBytes(path), UTF_8).trim(); } catch (IOException e) { throw new IOError(e); } return ExampleInfo.create(posOrNeg, checkerClass, fileName, code); } } private static final Predicate<ExampleInfo> IS_POSITIVE = new Predicate<ExampleInfo>() { @Override public boolean apply(ExampleInfo input) { return input.type() == ExampleInfo.ExampleKind.POSITIVE; } }; @Override public boolean processLine(String line) throws IOException { BugPatternInstance pattern = new Gson().fromJson(line, BugPatternInstance.class); 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 = new String(Files.readAllBytes(sidecarExplanation), UTF_8).trim(); } // Construct an appropriate page for this {@code BugPattern}. Include altNames if // there are any, and explain the correct way to suppress. ImmutableMap.Builder<String, Object> templateData = ImmutableMap.<String, Object>builder() .put("category", pattern.category) .put("severity", pattern.severity) .put("name", pattern.name) .put("summary", pattern.summary.trim()) .put("altNames", Joiner.on(", ").join(pattern.altNames)) .put("explanation", pattern.explanation.trim()); if (baseUrl != null) { templateData.put("baseUrl", baseUrl); } if (generateFrontMatter) { Map<String, String> frontmatterData = ImmutableMap.<String, String>builder() .put("title", pattern.name) .put("summary", pattern.summary) .put("layout", "bugpattern") .put("category", pattern.category.toString()) .put("severity", pattern.severity.toString()) .build(); DumperOptions options = new DumperOptions(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); Yaml yaml = new Yaml(options); Writer yamlWriter = new StringWriter(); yamlWriter.write("---\n"); yaml.dump(frontmatterData, yamlWriter); yamlWriter.write("---\n"); templateData.put("frontmatter", yamlWriter.toString()); } if (pattern.documentSuppression) { String suppression; switch (pattern.suppressibility) { case SUPPRESS_WARNINGS: suppression = String.format( "Suppress false positives by adding an `@SuppressWarnings(\"%s\")` " + "annotation to the enclosing element.", pattern.name); break; case CUSTOM_ANNOTATION: if (pattern.customSuppressionAnnotations.length == 1) { suppression = String.format( "Suppress false positives by adding the custom suppression annotation " + "`@%s` to the enclosing element.", pattern.customSuppressionAnnotations[0]); } else { suppression = String.format( "Suppress false positives by adding one of these custom suppression " + "annotations to the enclosing element: %s", COMMA_JOINER.join( Lists.transform( Arrays.asList(pattern.customSuppressionAnnotations), ANNOTATE_AND_CODIFY))); } break; case UNSUPPRESSIBLE: suppression = "This check may not be suppressed."; break; default: throw new AssertionError(pattern.suppressibility); } templateData.put("suppression", suppression); } MustacheFactory mf = new DefaultMustacheFactory(); Mustache mustache = mf.compile("com/google/errorprone/resources/bugpattern.mustache"); mustache.execute(writer, templateData.build()); if (pattern.generateExamplesFromTestCases) { // Example filename must match example pattern. List<Path> examplePaths = new ArrayList<>(); Filter<Path> filter = new ExampleFilter(pattern.className.substring(pattern.className.lastIndexOf('.') + 1)); findExamples(examplePaths, exampleDirBase, filter); List<ExampleInfo> exampleInfos = FluentIterable.from(examplePaths) .transform(new PathToExampleInfo(pattern.className)) .toSortedList( // sort by name new Comparator<ExampleInfo>() { @Override public int compare(ExampleInfo first, ExampleInfo second) { return first.name().compareTo(second.name()); } }); Collection<ExampleInfo> positiveExamples = Collections2.filter(exampleInfos, IS_POSITIVE); Collection<ExampleInfo> negativeExamples = Collections2.filter(exampleInfos, not(IS_POSITIVE)); if (!exampleInfos.isEmpty()) { writer.write("\n----------\n\n"); if (!positiveExamples.isEmpty()) { writer.write("### Positive examples\n"); for (ExampleInfo positiveExample : positiveExamples) { writeExample(positiveExample, writer); } } if (!negativeExamples.isEmpty()) { writer.write("### Negative examples\n"); for (ExampleInfo negativeExample : negativeExamples) { writeExample(negativeExample, writer); } } } } } return true; } private void writeExample(ExampleInfo example, Writer writer) throws IOException { writer.write("__" + example.name() + "__\n\n"); if (usePygments) { writer.write("{% highlight java %}\n"); } else { writer.write("```java\n"); } writer.write(example.code() + "\n"); if (usePygments) { writer.write("{% endhighlight %}\n"); } else { writer.write("```\n"); } writer.write("\n"); } private static void findExamples( List<Path> examples, Path dir, DirectoryStream.Filter<Path> filter) throws IOException { try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) { for (Path entry : stream) { if (Files.isDirectory(entry)) { findExamples(examples, entry, filter); } else { examples.add(entry); } } } } @Override public List<BugPatternInstance> getResult() { return result; } }