
com.android.tools.lint.MaterialHtmlReporter Maven / Gradle / Ivy
Show all versions of lint Show documentation
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.android.tools.lint;
import static com.android.SdkConstants.DOT_JPG;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.tools.lint.detector.api.LintUtils.endsWith;
import static com.android.tools.lint.detector.api.TextFormat.HTML;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.utils.HtmlBuilder;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.Maps;
import com.google.common.collect.ObjectArrays;
import com.google.common.io.Files;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A reporter which emits lint results into an HTML report.
* Like {@link HtmlReporter} but uses a newer Material style.
*
* NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.
*/
@Beta
public class MaterialHtmlReporter extends Reporter {
/**
* Maximum number of warnings allowed for a single issue type before we
* split up and hide all but the first {@link #SHOWN_COUNT} items.
*/
private static final int SPLIT_LIMIT;
/**
* When a warning has at least {@link #SPLIT_LIMIT} items, then we show the
* following number of items before the "Show more" button/link.
*/
private static final int SHOWN_COUNT;
/** Number of lines to show around code snippets */
static final int CODE_WINDOW_SIZE;
private static final String REPORT_PREFERENCE_PROPERTY = "lint.html.prefs";
private static final boolean USE_WAVY_UNDERLINES_FOR_ERRORS;
/**
* Whether we should try to use browser support for wavy underlines.
* Underlines are not working well; see https://bugs.chromium.org/p/chromium/issues/detail?id=165462 for when to re-enable.
* If false we're using a CSS trick with repeated images instead.
* (Only applies if {@link #USE_WAVY_UNDERLINES_FOR_ERRORS} is true.)
*/
private static final boolean USE_CSS_DECORATION_FOR_WAVY_UNDERLINES = false;
private static String preferredThemeName = "light";
static {
String preferences = System.getProperty(REPORT_PREFERENCE_PROPERTY);
int codeWindowSize = 3;
int splitLimit = 8;
boolean underlineErrors = true;
if (preferences != null) {
for (String pref : Splitter.on(',').omitEmptyStrings().split(preferences)) {
int index = pref.indexOf('=');
if (index != -1) {
String key = pref.substring(0, index).trim();
String value = pref.substring(index+1).trim();
if ("theme".equals(key)) {
preferredThemeName = value;
} else if ("window".equals(key)) {
try {
int size = Integer.decode(value);
if (size >= 1 && size < 3000) {
codeWindowSize = size;
}
} catch (NumberFormatException ignore) {
}
} else if ("maxPerIssue".equals(key)) {
try {
int count = Integer.decode(value);
if (count >= 1 && count < 3000) {
splitLimit = count;
}
} catch (NumberFormatException ignore) {
}
} else if ("underlineErrors".equals(key)) {
underlineErrors = Boolean.valueOf(value);
}
}
}
}
SPLIT_LIMIT = splitLimit;
SHOWN_COUNT = Math.max(1, SPLIT_LIMIT - 3);
CODE_WINDOW_SIZE = codeWindowSize;
USE_WAVY_UNDERLINES_FOR_ERRORS = underlineErrors;
}
/**
* CSS themes for syntax highlighting. The following classes map to an IntelliJ color
* theme like this:
*
* - pre.errorlines: General > Text > Default Text
*
- .prefix: XML > Namespace Prefix
*
- .attribute: XML > Attribute name
*
- .value: XML > Attribute value
*
- .tag: XML > Tag name
*
- .comment: XML > Comment
*
- .javado: Comments > JavaDoc > Text
*
- .annotation: Java > Annotations > Annotation name
*
- .string: Java > String > String text
*
- .number: Java > Numbers
*
- .keyword: Java > Keyword
*
- .caretline: General > Editor > Caret row (Background)
*
- .lineno: For color, General > Code > Line number, Foreground, and for
* background-color, Editor > Gutter background
*
- .error: General > Errors and Warnings > Error
*
- .warning: General > Errors and Warnings > Warning
*
- text-decoration: none;\n"
*
*/
@SuppressWarnings("ConstantConditions")
private static final String CSS_SYNTAX_COLORS_LIGHT_THEME = ""
// Syntax highlighting
+ "pre.errorlines {\n"
+ " background-color: white;\n"
+ " font-family: monospace;\n"
+ " border: 1px solid #e0e0e0;\n"
+ " line-height: 0.9rem;\n" // ensure line number gutter looks contiguous
+ " font-size: 0.9rem;"
+ " padding: 1px 0px 1px; 1px;\n" // no padding to make gutter look better
+ " overflow: scroll;\n"
+ "}\n"
+ ".prefix {\n"
+ " color: #660e7a;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".attribute {\n"
+ " color: #0000ff;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".value {\n"
+ " color: #008000;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".tag {\n"
+ " color: #000080;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".comment {\n"
+ " color: #808080;\n"
+ " font-style: italic;\n"
+ "}\n"
+ ".javadoc {\n"
+ " color: #808080;\n"
+ " font-style: italic;\n"
+ "}\n"
+ ".annotation {\n"
+ " color: #808000;\n"
+ "}\n"
+ ".string {\n"
+ " color: #008000;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".number {\n"
+ " color: #0000ff;\n"
+ "}\n"
+ ".keyword {\n"
+ " color: #000080;\n"
+ " font-weight: bold;\n"
+ "}\n"
+ ".caretline {\n"
+ " background-color: #fffae3;\n"
+ "}\n"
+ ".lineno {\n"
+ " color: #999999;\n"
+ " background-color: #f0f0f0;\n"
+ "}\n"
+ ".error {\n"
+ (USE_WAVY_UNDERLINES_FOR_ERRORS ? (USE_CSS_DECORATION_FOR_WAVY_UNDERLINES ? ""
+ " text-decoration: underline wavy #ff0000;\n"
+ " text-decoration-color: #ff0000;\n"
+ " -webkit-text-decoration-color: #ff0000;\n"
+ " -moz-text-decoration-color: #ff0000;\n"
+ "" : ""
+ " display: inline-block;\n"
+ " position:relative;\n"
+ " background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AwCFR4T/3uLMgAAADxJREFUCNdNyLERQEAABMCjL4lQwIzcjErpguAL+C9AvgKJDbeD/PRpLdm35Hm+MU+cB+tCKaJW4L4YBy+CAiLJrFs9mgAAAABJRU5ErkJggg==) bottom repeat-x;\n"
+ "") : ""
+ " text-decoration: none;\n"
+ " background-color: #f8d8d8;\n")
+ "}\n"
+ ".warning {\n"
+ " text-decoration: none;\n"
+ " background-color: #f6ebbc;\n"
+ "}\n";
@SuppressWarnings("ConstantConditions")
private static final String CSS_SYNTAX_COLORS_DARCULA = ""
+ "pre.errorlines {\n"
+ " background-color: #2b2b2b;\n"
+ " color: #a9b7c6;\n"
+ " font-family: monospace;\n"
+ " font-size: 0.9rem;"
+ " line-height: 0.9rem;\n" // ensure line number gutter looks contiguous
+ " padding: 6px;\n"
+ " border: 1px solid #e0e0e0;\n"
+ " overflow: scroll;\n"
+ "}\n"
+ ".prefix {\n"
+ " color: #9876aa;\n"
+ "}\n"
+ ".attribute {\n"
+ " color: #BABABA;\n"
+ "}\n"
+ ".value {\n"
+ " color: #6a8759;\n"
+ "}\n"
+ ".tag {\n"
+ " color: #e8bf6a;\n"
+ "}\n"
+ ".comment {\n"
+ " color: #808080;\n"
+ "}\n"
+ ".javadoc {\n"
+ " font-style: italic;\n"
+ " color: #629755;\n"
+ "}\n"
+ ".annotation {\n"
+ " color: #BBB529;\n"
+ "}\n"
+ ".string {\n"
+ " color: #6a8759;\n"
+ "}\n"
+ ".number {\n"
+ " color: #6897bb;\n"
+ "}\n"
+ ".keyword {\n"
+ " color: #cc7832;\n"
+ "}\n"
+ ".caretline {\n"
+ " background-color: #323232;\n"
+ "}\n"
+ ".lineno {\n"
+ " color: #606366;\n"
+ " background-color: #313335;\n"
+ "}\n"
+ ".error {\n"
+ (USE_WAVY_UNDERLINES_FOR_ERRORS ? (USE_CSS_DECORATION_FOR_WAVY_UNDERLINES ? ""
+ " text-decoration: underline wavy #ff0000;\n"
+ " text-decoration-color: #ff0000;\n"
+ " -webkit-text-decoration-color: #ff0000;\n"
+ " -moz-text-decoration-color: #ff0000;\n"
+ "" : ""
+ " display: inline-block;\n"
+ " position:relative;\n"
+ " background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AwCFR46vckTXgAAAEBJREFUCNdj1NbW/s+ABJj4mJgYork5GNgZGSECYVzsDKd+/WaI5uZgEGVmYmBZ9e0nw6d//xg+/vvJEM7FwQAAPnUOmQBDSmAAAAAASUVORK5CYII=) bottom repeat-x;\n"
+ "") : ""
+ " text-decoration: none;\n"
+ " background-color: #52503a;\n")
+ "}\n"
+ ".warning {\n"
+ " text-decoration: none;\n"
+ " background-color: #52503a;\n"
+ "}\n";
/** Solarized theme. */
@SuppressWarnings({"ConstantConditions", "SpellCheckingInspection"})
private static final String CSS_SYNTAX_COLORS_SOLARIZED = ""
+ "pre.errorlines {\n"
+ " background-color: #FDF6E3;\n" // General > Text > Default Text, Background
+ " color: #586E75;\n" // General > Text > Default text, Foreground
+ " font-family: monospace;\n"
+ " font-size: 0.9rem;"
+ " line-height: 0.9rem;\n" // ensure line number gutter looks contiguous
+ " padding: 0px;\n" // no padding to make gutter look better
+ " border: 1px solid #e0e0e0;\n"
+ " overflow: scroll;\n"
+ "}\n"
+ ".prefix {\n" // XML > Namespace Prefix
+ " color: #6C71C4;\n"
+ "}\n"
+ ".attribute {\n" // XML > Attribute name
+ "}\n"
+ ".value {\n" // XML > Attribute value
+ " color: #2AA198;\n"
+ "}\n"
+ ".tag {\n" // XML > Tag name
+ " color: #268BD2;\n"
+ "}\n"
+ ".comment {\n" // XML > Comment
+ " color: #DC322F;\n"
+ "}\n"
+ ".javadoc {\n" // Comments > JavaDoc > Text
+ " font-style: italic;\n"
+ " color: #859900;\n"
+ "}\n"
+ ".annotation {\n" // Java > Annotations > Annotation name
+ " color: #859900;\n"
+ "}\n"
+ ".string {\n" // Java > String > String text
+ " color: #2AA198;\n"
+ "}\n"
+ ".number {\n" // Java > Numbers
+ " color: #CB4B16;\n"
+ "}\n"
+ ".keyword {\n" // Java > Keyword
+ " color: #B58900;\n"
+ "}\n"
+ ".caretline {\n" // General > Editor > Caret row, Background
+ " background-color: #EEE8D5;\n"
+ "}\n"
+ ".lineno {\n"
+ " color: #93A1A1;\n" // General > Code > Line number, Foreground
+ " background-color: #EEE8D5;\n" // Editor > Gutter background, Background
+ "}\n"
+ ".error {\n" // General > Errors and Warnings > Error
+ (USE_WAVY_UNDERLINES_FOR_ERRORS ? (USE_CSS_DECORATION_FOR_WAVY_UNDERLINES ? ""
+ " text-decoration: underline wavy #DC322F;\n"
+ " text-decoration-color: #DC322F;\n"
+ " -webkit-text-decoration-color: #DC322F;\n"
+ " -moz-text-decoration-color: #DC322F;\n"
+ "" : ""
+ " display: inline-block;\n"
+ " position:relative;\n"
+ " background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4AwCFRgHs/v4yQAAAD5JREFUCNcBMwDM/wDqe2//++zZ//324v/75NH/AgxKRgDuho8A/OTnAO2KkwAA/fbi//nXxf/mZlz/++TR/4EMI0ZH4MfyAAAAAElFTkSuQmCC) bottom repeat-x;\n"
+ "") : ""
+ " text-decoration: none;\n"
+ " color: #073642;\n" // not from theme
+ " background-color: #FFA0A3;\n") // not from theme
+ "}\n"
+ ".warning {\n" // General > Errors and Warnings > Warning
+ " text-decoration: none;\n"
+ " color: #073642;\n"
+ " background-color: #FFDF80;\n"
+ "}\n";
private static final String CSS_SYNTAX_COLORS;
static {
String css;
switch (preferredThemeName) {
case "darcula":
css = CSS_SYNTAX_COLORS_DARCULA; break;
case "solarized":
css = CSS_SYNTAX_COLORS_SOLARIZED; break;
case "light":
default:
css = CSS_SYNTAX_COLORS_LIGHT_THEME; break;
}
CSS_SYNTAX_COLORS = css;
}
/**
* Stylesheet for the HTML report.
* Note that the {@link LintSyntaxHighlighter} also depends on these class names.
*/
static final String CSS_STYLES = ""
+ "section.section--center {\n"
+ " max-width: 860px;\n"
+ "}\n"
+ ".mdl-card__supporting-text + .mdl-card__actions {\n"
+ " border-top: 1px solid rgba(0, 0, 0, 0.12);\n"
+ "}\n"
+ "main > .mdl-layout__tab-panel {\n"
+ " padding: 8px;\n"
+ " padding-top: 48px;\n"
+ "}\n"
+ "\n"
+ ".mdl-card__actions {\n"
+ " margin: 0;\n"
+ " padding: 4px 40px;\n"
+ " color: inherit;\n"
+ "}\n"
+ ".mdl-card > * {\n"
+ " height: auto;\n"
+ "}\n"
+ ".mdl-card__actions a {\n"
+ " color: #00BCD4;\n"
+ " margin: 0;\n"
+ "}\n"
+ ".error-icon {\n"
+ " color: #bb7777;\n"
+ " vertical-align: bottom;\n"
+ "}\n"
+ ".warning-icon {\n"
+ " vertical-align: bottom;\n"
+ "}\n"
+ ".mdl-layout__content section:not(:last-of-type) {\n"
+ " position: relative;\n"
+ " margin-bottom: 48px;\n"
+ "}\n"
+ "\n"
+ ".mdl-card .mdl-card__supporting-text {\n"
+ " margin: 40px;\n"
+ " -webkit-flex-grow: 1;\n"
+ " -ms-flex-positive: 1;\n"
+ " flex-grow: 1;\n"
+ " padding: 0;\n"
+ " color: inherit;\n"
+ " width: calc(100% - 80px);\n"
+ "}\n"
// Bug workaround - without this the hamburger icon is off center
+ "div.mdl-layout__drawer-button .material-icons {\n"
+ " line-height: 48px;\n"
+ "}\n"
// Make titles look better:
+ ".mdl-card .mdl-card__supporting-text {\n"
+ " margin-top: 0px;\n"
+ "}\n"
+ ".chips {\n"
+ " float: right;\n"
+ " vertical-align: middle;\n"
+ "}\n"
+ CSS_SYNTAX_COLORS
+ ".overview {\n"
+ " padding: 10pt;\n"
+ " width: 100%;\n"
+ " overflow: auto;\n"
+ " border-collapse:collapse;\n"
+ "}\n"
+ ".overview tr {\n"
+ " border-bottom: solid 1px #eeeeee;\n"
+ "}\n"
+ ".categoryColumn a {\n"
+ " text-decoration: none;\n"
+ " color: inherit;\n"
+ "}\n"
+ ".countColumn {\n"
+ " text-align: right;\n"
+ " padding-right: 20px;\n"
+ " width: 50px;\n"
+ "}\n"
+ ".issueColumn {\n"
+ " padding-left: 16px;\n"
+ "}\n"
+ ".categoryColumn {\n"
+ " position: relative;\n"
+ " left: -50px;\n"
+ " padding-top: 20px;\n"
+ " padding-bottom: 5px;\n"
+ "}\n";
protected final Writer writer;
protected final LintCliFlags flags;
private HtmlBuilder builder;
@SuppressWarnings("StringBufferField")
private StringBuilder sb;
private String highlightedFile;
private LintSyntaxHighlighter highlighter;
/**
* Creates a new {@link MaterialHtmlReporter}
*
* @param client the associated client
* @param output the output file
* @param flags the command line flags
* @throws IOException if an error occurs
*/
public MaterialHtmlReporter(
@NonNull LintCliClient client,
@NonNull File output,
@NonNull LintCliFlags flags) throws IOException {
super(client, output);
writer = new BufferedWriter(Files.newWriter(output, Charsets.UTF_8));
this.flags = flags;
}
@Override
public void write(@NonNull Stats stats, List issues) throws IOException {
Map missing = computeMissingIssues(issues);
List> related = computeIssueLists(issues);
startReport(stats);
writeNavigationHeader(stats, () -> {
append(" "
+ "dashboardOverview\n");
for (List warnings : related) {
Warning first = warnings.get(0);
String anchor = first.issue.getId();
String desc = first.issue.getBriefDescription(TextFormat.HTML);
append(" ");
if (first.severity.isError()) {
append("error");
} else {
append("warning");
}
append(desc + " (" + warnings.size() + ")\n");
}
});
if (!issues.isEmpty()) {
append("\n\n");
writeCard(() -> writeOverview(related, missing.size()), "Overview", true);
Category previousCategory = null;
for (List warnings : related) {
Category category = warnings.get(0).issue.getCategory();
if (category != previousCategory) {
previousCategory = category;
append("\n\n");
}
writeIssueCard(warnings);
}
if (!client.isCheckingSpecificIssues()) {
writeMissingIssues(missing);
}
writeSuppressIssuesCard();
} else {
writeCard(() -> append("Congratulations!"), "No Issues Found");
}
finishReport();
writeReport();
if (!client.getFlags().isQuiet()
&& (stats.errorCount > 0 || stats.warningCount > 0)) {
String url = SdkUtils.fileToUrlString(output.getAbsoluteFile());
System.out.println(String.format("Wrote HTML report to %1$s", url));
}
}
private void append(@NonNull String s) {
sb.append(s);
}
private void append(char s) {
sb.append(s);
}
private void writeSuppressIssuesCard() {
append("\n\n");
writeCard(() -> {
append(TextFormat.RAW.convertTo(Main.getSuppressHelp(), TextFormat.HTML));
this.append('\n');
}, "Suppressing Warnings and Errors");
}
private void writeIssueCard(List warnings) {
Issue firstIssue = warnings.get(0).issue;
append("\n");
writeCard(() -> {
Warning first = warnings.get(0);
Issue issue = first.issue;
append("\n");
append("\n");
boolean partialHide = !simpleFormat && warnings.size() > SPLIT_LIMIT;
int count = 0;
for (Warning warning : warnings) {
// Don't show thousands of matches for common errors; this just
// makes some reports huge and slow to render and nobody really wants to
// inspect 50+ individual reports of errors of the same type
if (count >= 50) {
if (count == 50) {
append("
NOTE: "
+ Integer.toString(warnings.size() - count)
+ " results omitted.
");
}
count++;
continue;
}
if (partialHide && count == SHOWN_COUNT) {
String id = warning.issue.getId() + "Div";
append("");
append(String.format("+ %1$d More Occurrences...",
warnings.size() - SHOWN_COUNT));
append("\n");
append("\n");
}
count++;
String url = null;
if (warning.path != null) {
url = writeLocation(warning.file, warning.path, warning.line);
append(':');
append(' ');
}
// Is the URL for a single image? If so, place it here near the top
// of the error floating on the right. If there are multiple images,
// they will instead be placed in a horizontal box below the error
boolean addedImage = false;
if (url != null && warning.location != null
&& warning.location.getSecondary() == null) {
addedImage = addImage(url, warning.location);
}
append("
");
} else {
append("
");
}
// Insert surrounding code block window
if (warning.line >= 0 && warning.fileContents != null) {
appendCodeBlock(warning.file, warning.fileContents,
warning.offset, warning.endOffset, warning.severity);
}
append('\n');
if (warning.location != null && warning.location.getSecondary() != null) {
append("");
Location l = warning.location.getSecondary();
int otherLocations = 0;
int shownSnippetsCount = 0;
while (l != null) {
String message = l.getMessage();
if (message != null && !message.isEmpty()) {
Position start = l.getStart();
int line = start != null ? start.getLine() : -1;
String path = client
.getDisplayPath(warning.project, l.getFile());
writeLocation(l.getFile(), path, line);
append(':');
append(' ');
append("
");
String name = l.getFile().getName();
// Only display up to 3 inlined views to keep big reports from
// getting massive in rendering cost
if (shownSnippetsCount < 3
&& !(endsWith(name, DOT_PNG) || endsWith(name,
DOT_JPG))) {
CharSequence s = client.readFile(l.getFile());
if (s.length() > 0) {
int offset = start != null ? start.getOffset() : -1;
appendCodeBlock(l.getFile(), s, offset, -1,
warning.severity);
}
shownSnippetsCount++;
}
} else {
otherLocations++;
}
l = l.getSecondary();
}
append("
");
if (otherLocations > 0) {
String id = "Location" + count + "Div";
append("");
append(String.format("+ %1$d Additional Locations...",
otherLocations));
append("\n");
append(" ");
append("\n");
append("Additional locations: ");
append("\n");
l = warning.location.getSecondary();
while (l != null) {
Position start = l.getStart();
int line = start != null ? start.getLine() : -1;
String path = client
.getDisplayPath(warning.project, l.getFile());
append("- ");
writeLocation(l.getFile(), path, line);
append("\n");
l = l.getSecondary();
}
append("
\n");
append("
\n");
}
}
// Place a block of images?
if (!addedImage && url != null && warning.location != null
&& warning.location.getSecondary() != null) {
addImage(url, warning.location);
}
if (warning.isVariantSpecific()) {
append("\n");
append("Applies to variants: ");
append(Joiner.on(", ").join(warning.getIncludedVariantNames()));
append("
\n");
append("Does not apply to variants: ");
append(Joiner.on(", ").join(warning.getExcludedVariantNames()));
append("
\n");
}
}
if (partialHide) { // Close up the extra div
append("\n"); // partial hide
}
append(" ");
if (addedImage) {
append("\n"); // class=warningslist
writeIssueMetadata(issue, null, true);
append("\n"); // class=issue
append("\n");
writeChip(issue.getId());
Category category = issue.getCategory();
while (category != null && category != Category.LINT) {
writeChip(category.getName());
category = category.getParent();
}
writeChip(first.severity.getDescription());
writeChip("Priority " + issue.getPriority() + "/10");
append("\n"); //class=chips
}, XmlUtils.toXmlTextValue(firstIssue.getBriefDescription(TextFormat.TEXT)), true,
new Action("Explain", getExplanationId(firstIssue),
"reveal")); // HTML style isn't handled right by card widget
}
/**
* Sorts the list of warnings into a list of lists where each list contains warnings
* for the same base issue type
*/
@NonNull
private static List> computeIssueLists(@NonNull List issues) {
Issue previousIssue = null;
List> related = new ArrayList<>();
if (!issues.isEmpty()) {
List currentList = null;
for (Warning warning : issues) {
if (warning.issue != previousIssue) {
previousIssue = warning.issue;
currentList = new ArrayList<>();
related.add(currentList);
}
assert currentList != null;
currentList.add(warning);
}
}
return related;
}
private void startReport(@NonNull Stats stats) {
sb = new StringBuilder(1800 * stats.count());
builder = new HtmlBuilder(sb);
writeOpenHtmlTag();
writeHeadTag();
writeOpenBodyTag();
}
private void finishReport() {
writeCloseNavigationHeader();
writeCloseBodyTag();
writeCloseHtmlTag();
}
private void writeNavigationHeader(@NonNull Stats stats, @NonNull Runnable appender) {
append(""
+ "\n"
+ " \n"
+ " \n"
+ " " + title + ": "
+ LintUtils.describeCounts(stats.errorCount, stats.warningCount, false)
+ "\n"
+ " \n"
+ " \n"
+ " \n"
+ " \n"
+ " \n"
+ " Issue Types\n"
+ " \n"
+ " \n"
+ " \n"
+ " ");
}
private void writeCloseNavigationHeader() {
append(""
+ " \n"
+ " \n"
+ "");
}
private void writeOpenBodyTag() {
append("" +
"\n");
}
private void writeCloseBodyTag() {
append("\n\n");
}
private void writeOpenHtmlTag() {
append(""
+ "\n"
+ "\n");
}
private void writeCloseHtmlTag() {
append("");
}
private void writeHeadTag() {
append(""
+ "\n"
+ "\n"
+ "\n"
+ "" + title + " \n");
// Material
append(""
+ "\n"
// Based on https://getmdl.io/customize/index.html
//+ "\n"
//+ " \n"
+ " \n"
+ "\n"
+ "\n");
append("\n");
// JavaScript for collapsing/expanding long lists
append(""
+ "\n");
append("\n");
}
private void writeIssueMetadata(Issue issue, String disabledBy, boolean hide) {
append(" \n"); //class=explanation
}
protected Map computeMissingIssues(List warnings) {
Set projects = new HashSet<>();
Set seen = new HashSet<>();
for (Warning warning : warnings) {
projects.add(warning.project);
seen.add(warning.issue);
}
Configuration cliConfiguration = client.getConfiguration();
Map map = Maps.newHashMap();
for (Issue issue : client.getRegistry().getIssues()) {
if (!seen.contains(issue)) {
if (client.isSuppressed(issue)) {
map.put(issue, "Command line flag");
continue;
}
if (!issue.isEnabledByDefault() && !client.isAllEnabled()) {
map.put(issue, "Default");
continue;
}
if (cliConfiguration != null && !cliConfiguration.isEnabled(issue)) {
map.put(issue, "Command line supplied --config lint.xml file");
continue;
}
// See if any projects disable this warning
for (Project project : projects) {
if (!project.getConfiguration(null).isEnabled(issue)) {
map.put(issue, "Project lint.xml file");
break;
}
}
}
}
return map;
}
private void writeMissingIssues(@NonNull Map missing) {
if (!client.isCheckingSpecificIssues()) {
append("\n\n");
writeCard(() -> {
append(""
+ "One or more issues were not run by lint, either \n"
+ "because the check is not enabled by default, or because \n"
+ "it was disabled with a command line flag or via one or \n"
+ "more lint.xml
configuration files in the project "
+ "directories.\n");
append("");
List list = new ArrayList<>(missing.keySet());
Collections.sort(list);
append("
");
for (Issue issue : list) {
append("\n");
// Explain this issue
append("");
append(issue.getId());
append("\n");
append("\n");
String disabledBy = missing.get(issue);
writeIssueMetadata(issue, disabledBy, false);
append("\n");
}
append(" "); //SuppressedIssues
}, "Disabled Checks", true,
new Action("List Missing Issues", "SuppressedIssues", "reveal"));
}
}
private void writeOverview(List> related, int missingCount) {
// Write issue id summary
append("\n");
Category previousCategory = null;
for (List warnings : related) {
Warning first = warnings.get(0);
Issue issue = first.issue;
boolean isError = first.severity.isError();
if (issue.getCategory() != previousCategory) {
append("");
previousCategory = issue.getCategory();
String categoryName = issue.getCategory().getFullName();
append("");
append(categoryName);
append("\n");
append(" ");
append("\n");
}
append("\n");
// Count column
append("");
append(Integer.toString(warnings.size()));
append(" ");
append("");
if (isError) {
append("error");
} else {
append("warning");
}
append('\n');
append("");
append(issue.getId());
append("");
append(": ");
append(issue.getBriefDescription(HTML));
append(" \n");
}
if (missingCount > 0 && !client.isCheckingSpecificIssues()) {
append(" ");
append("");
append("");
append(String.format("Disabled Checks (%1$d)",
missingCount));
append("\n");
append(" ");
}
append("
\n");
append("
");
}
private static String getCardId(int cardNumber) {
return "card" + cardNumber;
}
private static String getExplanationId(Issue issue) {
return "explanation" + issue.getId();
}
public void writeCardHeader(String title, int cardNumber) {
append(""
+ "\n"
+ " \n");
if (title != null) {
append(""
+ " \n"
+ " " + title + "
\n"
+ " \n");
}
append(""
+ " \n");
}
private static class Action {
public String title;
public String id;
public String function;
public Action(String title, String id, String function) {
this.title = title;
this.id = id;
this.function = function;
}
}
public void writeCardAction(@NonNull Action... actions) {
append(""
+ " \n"
+ " \n");
for (Action action : actions) {
append(""
+ "");
}
}
public void writeCardFooter() {
append(""
+ " \n"
+ " \n"
+ " ");
}
public void writeCard(@NonNull Runnable appender, @Nullable String title) {
writeCard(appender, title, false);
}
public void writeChip(@NonNull String text) {
append(""
+ "\n"
+ " " + text + "\n"
+ "\n");
}
int cardNumber = 0;
public void writeCard(@NonNull Runnable appender, @Nullable String title, boolean dismissible,
Action... actions) {
int card = cardNumber++;
writeCardHeader(title, card);
appender.run();
if (dismissible) {
String dismissTitle = "Dismiss";
if ("New Lint Report Format".equals(title)) {
dismissTitle = "Got It";
}
actions = ObjectArrays.concat(actions, new Action(dismissTitle, getCardId(card),
"hideid"));
writeCardAction(actions);
}
writeCardFooter();
}
private String writeLocation(File file, String path, int line) {
String url;
append("");
url = getUrl(file);
if (url != null) {
append("");
}
String displayPath = stripPath(path);
if (url != null && url.startsWith("../") && new File(displayPath).isAbsolute()) {
displayPath = url;
}
append(displayPath);
//noinspection VariableNotUsedInsideIf
if (url != null) {
append("");
}
if (line >= 0) {
// 0-based line numbers, but display 1-based
append(':');
append(Integer.toString(line + 1));
}
append("");
return url;
}
private boolean addImage(String url, Location location) {
if (url != null && endsWith(url, DOT_PNG)) {
if (location.getSecondary() != null) {
// Emit many images
// Add in linked images as well
List urls = new ArrayList<>();
while (location != null) {
String imageUrl = getUrl(location.getFile());
if (imageUrl != null
&& endsWith(imageUrl, DOT_PNG)) {
urls.add(imageUrl);
}
location = location.getSecondary();
}
if (!urls.isEmpty()) {
// Sort in order
urls.sort(Comparator.comparingInt(HtmlReporter::getDpiRank));
append("");
append("");
for (String linkedUrl : urls) {
// Image series: align top
append("");
append("");
append("
\n");
append(" ");
}
append(" ");
append("");
for (String linkedUrl : urls) {
append("");
int index = linkedUrl.lastIndexOf("drawable-");
if (index != -1) {
index += "drawable-".length();
int end = linkedUrl.indexOf('/', index);
if (end != -1) {
append(linkedUrl.substring(index, end));
}
}
append(" ");
}
append(" \n");
append("
\n");
}
} else {
// Just this image: float to the right
append("
");
}
return true;
}
return false;
}
@Override
public void writeProjectList(@NonNull Stats stats,
@NonNull List projects) throws IOException {
startReport(stats);
writeNavigationHeader(stats, () -> {
for (MultiProjectHtmlReporter.ProjectEntry entry : projects) {
append(" "
+ entry.path + " (" + (entry.errorCount + entry.warningCount) + ")\n");
}
});
if (stats.errorCount == 0 && stats.warningCount == 0) {
writeCard(() -> append("Congratulations!"), "No Issues Found");
return;
}
writeCard(() -> {
// Write issue id summary
append("\n");
append("");
append("Project");
append(" ");
append("Errors");
append(" ");
append("Warnings");
append(" \n");
for (MultiProjectHtmlReporter.ProjectEntry entry : projects) {
append("");
append("");
append(entry.path);
append(" ");
append(Integer.toString(entry.errorCount));
append(" ");
append(Integer.toString(entry.warningCount));
append(" \n");
append("\n");
}
append("
\n");
append("
");
}, "Projects");
finishReport();
writeReport();
}
private void writeReport() throws IOException {
writer.write(sb.toString());
writer.close();
sb = null;
builder = null;
}
@NonNull
private LintSyntaxHighlighter getHighlighter(@NonNull File file,
@NonNull CharSequence contents) {
if (highlightedFile == null || !highlightedFile.equals(file.getPath())) {
highlighter = new LintSyntaxHighlighter(file.getName(), contents.toString());
highlighter.setPadCaretLine(true);
highlighter.setDedent(true);
highlightedFile = file.getPath();
}
return highlighter;
}
/** Insert syntax highlighted XML */
private void appendCodeBlock(@NonNull File file, @NonNull CharSequence contents,
int startOffset, int endOffset, @NonNull Severity severity) {
getHighlighter(file, contents).generateHtml(builder, startOffset, endOffset,
severity.isError());
}
}