All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.MaterialHtmlReporter Maven / Gradle / Ivy

Go to download

Lint tools. Both a Command line tool and a library to add lint features to other tools

The newest version!
/*
 * 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(""); append(RAW.convertTo(warning.message, HTML)); append(""); if (addedImage) { 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(""); append(RAW.convertTo(message, HTML)); 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("\n"); 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("
\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("
"); if (disabledBy != null) { append(String.format("Disabled By: %1$s
\n", disabledBy)); } append("
\n"); String explanationHtml = issue.getExplanation(HTML); append(explanationHtml); List moreInfo = issue.getMoreInfo(); append("
"); // TODO: Skip MoreInfo links already present in the HTML to avoid redundancy. int count = moreInfo.size(); if (count > 0) { append("
"); append("More info: "); if (count > 1) { append("
    "); } for (String uri : moreInfo) { if (count > 1) { append("
  • "); } append(""); append(uri); append("\n"); } if (count > 1) { append("
"); } append("
"); } append("
"); if (client.getRegistry() instanceof BuiltinIssueRegistry) { if (Reporter.hasAutoFix(issue)) { append( "Note: This issue has an associated quickfix operation in Android Studio and IntelliJ IDEA."); append("
\n"); } } append(String.format( "To suppress this error, use the issue id \"%1$s\" as explained in the " + "%2$sSuppressing Warnings and Errors%3$s section.", issue.getId(), "", "")); append("
\n"); append("
"); //class=moreinfo append("\n
\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(""); append("\n"); } append("\n"); // Count column append(""); append("\n"); } if (missingCount > 0 && !client.isCheckingSpecificIssues()) { append(""); append(""); } append("
"); previousCategory = issue.getCategory(); String categoryName = issue.getCategory().getFullName(); append(""); append(categoryName); append("\n"); append("
"); append(Integer.toString(warnings.size())); append(""); if (isError) { append("error"); } else { append("warning"); } append('\n'); append(""); append(issue.getId()); append(""); append(": "); append(issue.getBriefDescription(HTML)); append("
"); append(""); append(String.format("Disabled Checks (%1$d)", missingCount)); append("\n"); 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(""); for (String linkedUrl : urls) { append(""); } append("\n"); append("
"); append(""); append("\n"); 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("
\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("\n"); for (MultiProjectHtmlReporter.ProjectEntry entry : projects) { append("\n"); append("\n"); } append("
"); append("Project"); append(""); append("Errors"); append(""); append("Warnings"); append("
"); append(""); append(entry.path); append(""); append(Integer.toString(entry.errorCount)); append(""); append(Integer.toString(entry.warningCount)); 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()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy