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

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

/*
 * Copyright (C) 2011 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.Issue.OutputFormat.HTML;
import static com.android.tools.lint.detector.api.LintUtils.endsWith;

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.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.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import com.google.common.io.Files;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.net.URL;
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.
 * 

* 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 HtmlReporter extends Reporter { private static final boolean USE_HOLO_STYLE = true; @SuppressWarnings("ConstantConditions") private static final String CSS = USE_HOLO_STYLE ? "hololike.css" : "default.css"; //$NON-NLS-1$ //$NON-NLS-2$ /** * 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 = 8; /** * 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 = SPLIT_LIMIT - 3; protected final Writer mWriter; private String mStripPrefix; private String mFixUrl; /** * Creates a new {@link HtmlReporter} * * @param client the associated client * @param output the output file * @throws IOException if an error occurs */ public HtmlReporter(LintCliClient client, File output) throws IOException { super(client, output); mWriter = new BufferedWriter(Files.newWriter(output, Charsets.UTF_8)); } @Override public void write(int errorCount, int warningCount, List issues) throws IOException { Map missing = computeMissingIssues(issues); mWriter.write( "\n" + //$NON-NLS-1$ "\n" + //$NON-NLS-1$ "\n" + //$NON-NLS-1$ "" + //$NON-NLS-1$ "" + mTitle + "\n"); //$NON-NLS-1$//$NON-NLS-2$ writeStyleSheet(); if (!mSimpleFormat) { // JavaScript for collapsing/expanding long lists mWriter.write( "\n"); //$NON-NLS-1$ } mWriter.write( "\n" + //$NON-NLS-1$ "\n" + //$NON-NLS-1$ "

" + //$NON-NLS-1$ mTitle + "

\n" + //$NON-NLS-1$ "
\n"); //$NON-NLS-1$ mWriter.write(String.format("Check performed at %1$s.", new Date().toString())); mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write(String.format("%1$d errors and %2$d warnings found:", errorCount, warningCount)); mWriter.write("

\n"); //$NON-NLS-1$ Issue previousIssue = null; if (!issues.isEmpty()) { List> related = new ArrayList>(); 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); } writeOverview(related, missing.size()); Category previousCategory = null; for (List warnings : related) { Warning first = warnings.get(0); Issue issue = first.issue; if (issue.getCategory() != previousCategory) { previousCategory = issue.getCategory(); mWriter.write("\n\n"); //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ } mWriter.write("\n"); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("
\n"); //$NON-NLS-1$ // Explain this issue mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ boolean partialHide = !mSimpleFormat && warnings.size() > SPLIT_LIMIT; int count = 0; for (Warning warning : warnings) { if (partialHide && count == SHOWN_COUNT) { String id = warning.issue.getId() + "Div"; //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ } count++; String url = null; if (warning.path != null) { url = writeLocation(warning.file, warning.path, warning.line); mWriter.write(':'); mWriter.write(' '); } // 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); } mWriter.write(""); //$NON-NLS-1$ appendEscapedText(warning.message); mWriter.write(""); //$NON-NLS-1$ if (addedImage) { mWriter.write("
"); //$NON-NLS-1$ } else { mWriter.write("
"); //$NON-NLS-1$ } // Insert surrounding code block window if (warning.line >= 0 && warning.fileContents != null) { mWriter.write("
\n");   //$NON-NLS-1$
                        appendCodeBlock(warning.fileContents, warning.line, warning.offset);
                        mWriter.write("\n
"); //$NON-NLS-1$ } mWriter.write('\n'); if (warning.location != null && warning.location.getSecondary() != null) { mWriter.write("
    "); Location l = warning.location.getSecondary(); int otherLocations = 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 = mClient.getDisplayPath(warning.project, l.getFile()); writeLocation(l.getFile(), path, line); mWriter.write(':'); mWriter.write(' '); mWriter.write(""); //$NON-NLS-1$ appendEscapedText(message); mWriter.write(""); //$NON-NLS-1$ mWriter.write("
    "); //$NON-NLS-1$ String name = l.getFile().getName(); if (!(endsWith(name, DOT_PNG) || endsWith(name, DOT_JPG))) { String s = mClient.readFile(l.getFile()); if (s != null && !s.isEmpty()) { mWriter.write("
    \n");   //$NON-NLS-1$
                                            int offset = start != null ? start.getOffset() : -1;
                                            appendCodeBlock(s, line, offset);
                                            mWriter.write("\n
    "); //$NON-NLS-1$ } } } else { otherLocations++; } l = l.getSecondary(); } mWriter.write("
"); if (otherLocations > 0) { String id = "Location" + count + "Div"; //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("Additional locations: "); mWriter.write("
    \n"); //$NON-NLS-1$ l = warning.location.getSecondary(); while (l != null) { Position start = l.getStart(); int line = start != null ? start.getLine() : -1; String path = mClient.getDisplayPath(warning.project, l.getFile()); mWriter.write("
  • "); //$NON-NLS-1$ writeLocation(l.getFile(), path, line); mWriter.write("\n"); //$NON-NLS-1$ l = l.getSecondary(); } mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("


\n"); //$NON-NLS-1$ } } // Place a block of images? if (!addedImage && url != null && warning.location != null && warning.location.getSecondary() != null) { addImage(url, warning.location); } if (warning.isVariantSpecific()) { mWriter.write("\n"); mWriter.write("Applies to variants: "); mWriter.write(Joiner.on(", ").join(warning.getIncludedVariantNames())); mWriter.write("
\n"); mWriter.write("Does not apply to variants: "); mWriter.write(Joiner.on(", ").join(warning.getExcludedVariantNames())); mWriter.write("
\n"); } } if (partialHide) { // Close up the extra div mWriter.write("
\n"); //$NON-NLS-1$ } mWriter.write("
\n"); //$NON-NLS-1$ writeIssueMetadata(issue, first.severity, null); mWriter.write("
\n"); //$NON-NLS-1$ } if (!mClient.isCheckingSpecificIssues()) { writeMissingIssues(missing); } writeSuppressInfo(); } else { mWriter.write("Congratulations!"); } mWriter.write("\n\n"); //$NON-NLS-1$ mWriter.close(); String path = mOutput.getAbsolutePath(); System.out.println(String.format("Wrote HTML report to %1$s", path)); } private void writeIssueMetadata(Issue issue, Severity severity, String disabledBy) throws IOException { mWriter.write("
"); //$NON-NLS-1$ if (mClient.getRegistry() instanceof BuiltinIssueRegistry && ((BuiltinIssueRegistry) mClient.getRegistry()).hasAutoFix("adt", issue)) { //$NON-NLS-1$ mWriter.write("Note: This issue has an associated quickfix operation in Eclipse/ADT"); if (mFixUrl != null) { mWriter.write(" \"Fix\"\n"); //$NON-NLS-1$ } mWriter.write("
\n"); } if (disabledBy != null) { mWriter.write(String.format("Disabled By: %1$s
\n", disabledBy)); } mWriter.write("Priority: "); mWriter.write(String.format("%1$d / 10", issue.getPriority())); mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("Category: "); mWriter.write(issue.getCategory().getFullName()); mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("Severity: "); if (severity == Severity.ERROR || severity == Severity.FATAL) { mWriter.write(""); //$NON-NLS-1$ } else if (severity == Severity.WARNING) { mWriter.write(""); //$NON-NLS-1$ } else { mWriter.write(""); //$NON-NLS-1$ } appendEscapedText(severity.getDescription()); mWriter.write(""); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("Explanation: "); String description = issue.getDescription(HTML); mWriter.write(description); if (!description.isEmpty() && Character.isLetter(description.charAt(description.length() - 1))) { mWriter.write('.'); } mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ String explanationHtml = issue.getExplanation(HTML); mWriter.write(explanationHtml); mWriter.write("\n
\n"); //$NON-NLS-1$; List moreInfo = issue.getMoreInfo(); mWriter.write("
"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write("More info: "); int count = moreInfo.size(); if (count > 1) { mWriter.write(""); //$NON-NLS-1$ } mWriter.write("
"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write(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(), "", "")); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("
\n"); } private void writeSuppressInfo() throws IOException { //getSuppressHelp mWriter.write("\n\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write("Suppressing Warnings and Errors"); mWriter.write("
\n");//$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ appendEscapedText(Main.getSuppressHelp()); mWriter.write('\n'); } 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 = mClient.getConfiguration(); Map map = Maps.newHashMap(); for (Issue issue : mClient.getRegistry().getIssues()) { if (!seen.contains(issue)) { if (mClient.isSuppressed(issue)) { map.put(issue, "Command line flag"); continue; } if (!issue.isEnabledByDefault() && !mClient.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().isEnabled(issue)) { map.put(issue, "Project lint.xml file"); break; } } } } return map; } private void writeMissingIssues(Map missing) throws IOException { mWriter.write("\n\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write("Disabled Checks"); mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write( "The following issues were not run by lint, either " + "because the check is not enabled by default, or because " + "it was disabled with a command line flag or via one or " + "more lint.xml configuration files in the project directories."); mWriter.write("\n

\n"); //$NON-NLS-1$ List list = new ArrayList(missing.keySet()); Collections.sort(list); for (Issue issue : list) { mWriter.write("\n"); //$NON-NLS-1$ //$NON-NLS-2$ mWriter.write("
\n"); //$NON-NLS-1$ // Explain this issue mWriter.write("
"); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ String disabledBy = missing.get(issue); writeIssueMetadata(issue, issue.getDefaultSeverity(), disabledBy); mWriter.write("
\n"); //$NON-NLS-1$ } } protected void writeStyleSheet() throws IOException { if (USE_HOLO_STYLE) { mWriter.write( "\n" );//$NON-NLS-1$ } URL cssUrl = HtmlReporter.class.getResource(CSS); if (mSimpleFormat) { // Inline the CSS mWriter.write("\n"); //$NON-NLS-1$ } else { String ref = addLocalResources(cssUrl); if (ref != null) { mWriter.write( "\n"); //$NON-NLS-1$ } } } private void writeOverview(List> related, int missingCount) throws IOException { // Write issue id summary mWriter.write("\n"); //$NON-NLS-1$ String errorUrl = null; String warningUrl = null; if (!mSimpleFormat) { errorUrl = addLocalResources(getErrorIconUrl()); warningUrl = addLocalResources(getWarningIconUrl()); mFixUrl = addLocalResources(HtmlReporter.class.getResource("lint-run.png")); //$NON-NLS-1$) } Category previousCategory = null; for (List warnings : related) { Issue issue = warnings.get(0).issue; boolean isError = false; for (Warning warning : warnings) { if (warning.severity == Severity.ERROR || warning.severity == Severity.FATAL) { isError = true; break; } } if (issue.getCategory() != previousCategory) { mWriter.write(""); //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ } mWriter.write("\n"); //$NON-NLS-1$ // Count column mWriter.write(""); //$NON-NLS-1$ mWriter.write("\n"); } if (missingCount > 0 && !mClient.isCheckingSpecificIssues()) { mWriter.write(""); //$NON-NLS-1$ mWriter.write(""); //$NON-NLS-1$ } mWriter.write("
"); previousCategory = issue.getCategory(); String categoryName = issue.getCategory().getFullName(); mWriter.write(""); //$NON-NLS-1$ mWriter.write(categoryName); mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write(Integer.toString(warnings.size())); mWriter.write(""); //$NON-NLS-1$ String imageUrl = isError ? errorUrl : warningUrl; if (imageUrl != null) { mWriter.write("\"");\n"); //$NON-NLS-1$ } mWriter.write(""); //$NON-NLS-1$ mWriter.write(issue.getId()); mWriter.write(": "); //$NON-NLS-1$ mWriter.write(issue.getBriefDescription(HTML)); mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write(""); //$NON-NLS-1$ mWriter.write(String.format("Disabled Checks (%1$d)", missingCount)); mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ } private String writeLocation(File file, String path, int line) throws IOException { String url; mWriter.write(""); //$NON-NLS-1$ url = getUrl(file); if (url != null) { mWriter.write(""); //$NON-NLS-1$ } mWriter.write(stripPath(path)); if (url != null) { mWriter.write(""); //$NON-NLS-1$ } if (line >= 0) { // 0-based line numbers, but display 1-based mWriter.write(':'); mWriter.write(Integer.toString(line + 1)); } mWriter.write(""); //$NON-NLS-1$ return url; } private boolean addImage(String url, Location location) throws IOException { 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 && location.getFile() != 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 Collections.sort(urls, new Comparator() { @Override public int compare(String s1, String s2) { return getDpiRank(s1) - getDpiRank(s2); } }); mWriter.write(""); //$NON-NLS-1$ mWriter.write(""); //$NON-NLS-1$ for (String linkedUrl : urls) { // Image series: align top mWriter.write(""); //$NON-NLS-1$ } mWriter.write(""); //$NON-NLS-1$ mWriter.write(""); //$NON-NLS-1$ for (String linkedUrl : urls) { mWriter.write(""); //$NON-NLS-1$ } mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ mWriter.write(""); //$NON-NLS-1$ mWriter.write("\n"); //$NON-NLS-1$ mWriter.write("
"); //$NON-NLS-1$ int index = linkedUrl.lastIndexOf("drawable-"); //$NON-NLS-1$ if (index != -1) { index += "drawable-".length(); //$NON-NLS-1$ int end = linkedUrl.indexOf('/', index); if (end != -1) { mWriter.write(linkedUrl.substring(index, end)); } } mWriter.write("
\n"); //$NON-NLS-1$ } } else { // Just this image: float to the right mWriter.write(""); //$NON-NLS-1$ } return true; } return false; } /** Provide a sorting rank for a url */ private static int getDpiRank(String url) { if (url.contains("-xhdpi")) { //$NON-NLS-1$ return 0; } else if (url.contains("-hdpi")) { //$NON-NLS-1$ return 1; } else if (url.contains("-mdpi")) { //$NON-NLS-1$ return 2; } else if (url.contains("-ldpi")) { //$NON-NLS-1$ return 3; } else { return 4; } } private void appendCodeBlock(String contents, int lineno, int offset) throws IOException { int max = lineno + 3; int min = lineno - 3; for (int l = min; l < max; l++) { if (l >= 0) { int lineOffset = LintCliClient.getLineOffset(contents, l); if (lineOffset == -1) { break; } mWriter.write(String.format("%1$4d ", (l + 1))); //$NON-NLS-1$ String line = LintCliClient.getLineOfOffset(contents, lineOffset); if (offset != -1 && lineOffset <= offset && lineOffset+line.length() >= offset) { // Text nodes do not always have correct lines/offsets //assert l == lineno; // This line contains the beginning of the offset // First print everything before int delta = offset - lineOffset; appendEscapedText(line.substring(0, delta)); mWriter.write(""); //$NON-NLS-1$ appendEscapedText(line.substring(delta)); mWriter.write(""); //$NON-NLS-1$ } else if (offset == -1 && l == lineno) { mWriter.write(""); //$NON-NLS-1$ appendEscapedText(line); mWriter.write(""); //$NON-NLS-1$ } else { appendEscapedText(line); } if (l < max - 1) { mWriter.write("\n"); //$NON-NLS-1$ } } } } protected void appendEscapedText(String textValue) throws IOException { for (int i = 0, n = textValue.length(); i < n; i++) { char c = textValue.charAt(i); if (c == '<') { mWriter.write("<"); //$NON-NLS-1$ } else if (c == '&') { mWriter.write("&"); //$NON-NLS-1$ } else if (c == '\n') { mWriter.write("
\n"); } else { if (c > 255) { mWriter.write("&#"); //$NON-NLS-1$ mWriter.write(Integer.toString(c)); mWriter.write(';'); } else { mWriter.write(c); } } } } private String stripPath(String path) { if (mStripPrefix != null && path.startsWith(mStripPrefix) && path.length() > mStripPrefix.length()) { int index = mStripPrefix.length(); if (path.charAt(index) == File.separatorChar) { index++; } return path.substring(index); } return path; } /** Sets path prefix to strip from displayed file names */ void setStripPrefix(String prefix) { mStripPrefix = prefix; } static URL getWarningIconUrl() { return HtmlReporter.class.getResource("lint-warning.png"); //$NON-NLS-1$ } static URL getErrorIconUrl() { return HtmlReporter.class.getResource("lint-error.png"); //$NON-NLS-1$ } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy