/* * 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.detector.api; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.client.api.Configuration; import com.android.tools.lint.client.api.IssueRegistry; import com.google.common.annotations.Beta; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.List; /** * An issue is a potential bug in an Android application. An issue is discovered * by a {@link Detector}, and has an associated {@link Severity}. * <p> * Issues and detectors are separate classes because a detector can discover * multiple different issues as it's analyzing code, and we want to be able to * different severities for different issues, the ability to suppress one but * not other issues from the same detector, and so on. * <p/> * <b>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.</b> */ @Beta public final class Issue implements Comparable<Issue> { private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$ private final String mId; private final String mDescription; private final String mExplanation; private final Category mCategory; private final int mPriority; private final Severity mSeverity; private String mMoreInfoUrl; private boolean mEnabledByDefault = true; private final EnumSet<Scope> mScope; private List<EnumSet<Scope>> mAnalysisScopes; private final Class<? extends Detector> mClass; // Use factory methods private Issue( @NonNull String id, @NonNull String description, @NonNull String explanation, @NonNull Category category, int priority, @NonNull Severity severity, @NonNull Class<? extends Detector> detectorClass, @NonNull EnumSet<Scope> scope) { super(); mId = id; mDescription = description; mExplanation = explanation; mCategory = category; mPriority = priority; mSeverity = severity; mClass = detectorClass; mScope = scope; } /** * Creates a new issue * * @param id the fixed id of the issue * @param description the quick summary of the issue (one line) * @param explanation a full explanation of the issue, with suggestions for * how to fix it * @param category the associated category, if any * @param priority the priority, a number from 1 to 10 with 10 being most * important/severe * @param severity the default severity of the issue * @param detectorClass the class of the detector to find this issue * @param scope the scope of files required to analyze this issue * @return a new {@link Issue} */ @NonNull public static Issue create( @NonNull String id, @NonNull String description, @NonNull String explanation, @NonNull Category category, int priority, @NonNull Severity severity, @NonNull Class<? extends Detector> detectorClass, @NonNull EnumSet<Scope> scope) { return new Issue(id, description, explanation, category, priority, severity, detectorClass, scope); } /** * Returns the unique id of this issue. These should not change over time * since they are used to persist the names of issues suppressed by the user * etc. It is typically a single camel-cased word. * * @return the associated fixed id, never null and always unique */ @NonNull public String getId() { return mId; } /** * Briefly (one line) describes the kinds of checks performed by this rule * * @return a quick summary of the issue, never null */ @NonNull public String getDescription() { return mDescription; } /** * Describes the error found by this rule, e.g. * "Buttons must define contentDescriptions". Preferably the explanation * should also contain a description of how the problem should be solved. * Additional info can be provided via {@link #getMoreInfo()}. * <p> * Note that the text may contain some simple markup, such as *'s around sentences * for bold text, and back quotes (`) for code fragments. You can obtain * the text without this markup by calling {@link #getExplanationAsSimpleText()}, * and you can obtain the text as annotated HTML by calling * {@link #getExplanationAsHtml()}. * * @return an explanation of the issue, never null. */ @NonNull public String getExplanation() { return mExplanation; } /** * Like {@link #getExplanation()}, but returns the text as properly escaped * and marked up HTML, where http URLs are linked, where words with asterisks * such as *this* are shown in bold, etc. * * @return the explanation of the issue, never null */ @NonNull public String getExplanationAsHtml() { return convertMarkup(mExplanation, true /* html */); } /** * Like {@link #getExplanation()}, but returns the text as properly escaped * and marked up HTML, where http URLs are linked, where words with asterisks * such as *this* are shown in bold, etc. * * @return the explanation of the issue, never null */ @NonNull public String getExplanationAsSimpleText() { return convertMarkup(mExplanation, false /* not html = text */); } /** * The primary category of the issue * * @return the primary category of the issue, never null */ @NonNull public Category getCategory() { return mCategory; } /** * Returns a priority, in the range 1-10, with 10 being the most severe and * 1 the least * * @return a priority from 1 to 10 */ public int getPriority() { return mPriority; } /** * Returns the default severity of the issues found by this detector (some * tools may allow the user to specify custom severities for detectors). * <p> * Note that even though the normal way for an issue to be disabled is for * the {@link Configuration} to return {@link Severity#IGNORE}, there is a * {@link #isEnabledByDefault()} method which can be used to turn off issues * by default. This is done rather than just having the severity as the only * attribute on the issue such that an issue can be configured with an * appropriate severity (such as {@link Severity#ERROR}) even when issues * are disabled by default for example because they are experimental or not * yet stable. * * @return the severity of the issues found by this detector */ @NonNull public Severity getDefaultSeverity() { return mSeverity; } /** * Returns a link (a URL string) to more information, or null * * @return a link to more information, or null */ @Nullable public String getMoreInfo() { return mMoreInfoUrl; } /** * Returns whether this issue should be enabled by default, unless the user * has explicitly disabled it. * * @return true if this issue should be enabled by default */ public boolean isEnabledByDefault() { return mEnabledByDefault; } /** * Returns the scope required to analyze the code to detect this issue. * This is determined by the detectors which reports the issue. * * @return the required scope */ @NonNull public EnumSet<Scope> getScope() { return mScope; } /** * Sorts the detectors alphabetically by id. This is intended to make it * convenient to store settings for detectors in a fixed order. It is not * intended as the order to be shown to the user; for that, a tool embedding * lint might consider the priorities, categories, severities etc of the * various detectors. * * @param other the {@link Issue} to compare this issue to */ @Override public int compareTo(Issue other) { return getId().compareTo(other.getId()); } /** * Sets a more info URL string * * @param moreInfoUrl url string * @return this, for constructor chaining */ @NonNull public Issue setMoreInfo(@NonNull String moreInfoUrl) { mMoreInfoUrl = moreInfoUrl; return this; } /** * Sets whether this issue is enabled by default. * * @param enabledByDefault whether the issue should be enabled by default * @return this, for constructor chaining */ @NonNull public Issue setEnabledByDefault(boolean enabledByDefault) { mEnabledByDefault = enabledByDefault; return this; } /** * Returns the sets of scopes required to analyze this issue, or null if all * scopes named by {@link Issue#getScope()} are necessary. Note that only * <b>one</b> match out of this collection is required, not all, and that * the scope set returned by {@link #getScope()} does not have to be returned * by this method, but is always implied to be included. * <p> * The scopes returned by {@link Issue#getScope()} list all the various * scopes that are <b>affected</b> by this issue, meaning the detector * should consider it. Frequently, the detector must analyze all these * scopes in order to properly decide whether an issue is found. For * example, the unused resource detector needs to consider both the XML * resource files and the Java source files in order to decide if a resource * is unused. If it analyzes just the Java files for example, it might * incorrectly conclude that a resource is unused because it did not * discover a resource reference in an XML file. * <p> * However, there are other issues where the issue can occur in a variety of * files, but the detector can consider each in isolation. For example, the * API checker is affected by both XML files and Java class files (detecting * both layout constructor references in XML layout files as well as code * references in .class files). It doesn't have to analyze both; it is * capable of incrementally analyzing just an XML file, or just a class * file, without considering the other. * <p> * The required scope list provides a list of scope sets that can be used to * analyze this issue. For each scope set, all the scopes must be matched by * the incremental analysis, but any one of the scope sets can be analyzed * in isolation. * <p> * The required scope list is not required to include the full scope set * returned by {@link #getScope()}; that set is always assumed to be * included. * <p> * NOTE: You would normally call {@link #isAdequate(EnumSet)} rather * than calling this method directly. * * @return a list of required scopes, or null. */ @Nullable public Collection<EnumSet<Scope>> getAnalysisScopes() { return mAnalysisScopes; } /** * Sets the collection of scopes that are allowed to be analyzed independently. * See the {@link #getAnalysisScopes()} method for a full explanation. * Note that you usually want to just call {@link #addAnalysisScope(EnumSet)} * instead of constructing a list up front and passing it in here. This * method exists primarily such that commonly used share sets of analysis * scopes can be reused and set directly. * * @param required the collection of scopes * @return this, for constructor chaining */ public Issue setAnalysisScopes(@Nullable List<EnumSet<Scope>> required) { mAnalysisScopes = required; return this; } /** * Returns true if the given scope is adequate for analyzing this issue. * This looks through the analysis scopes (see * {@link #addAnalysisScope(EnumSet)}) and if the scope passed in fully * covers at least one of them, or if it covers the scope of the issue * itself (see {@link #getScope()}, which should be a superset of all the * analysis scopes) returns true. * <p> * The scope set returned by {@link Issue#getScope()} lists all the various * scopes that are <b>affected</b> by this issue, meaning the detector * should consider it. Frequently, the detector must analyze all these * scopes in order to properly decide whether an issue is found. For * example, the unused resource detector needs to consider both the XML * resource files and the Java source files in order to decide if a resource * is unused. If it analyzes just the Java files for example, it might * incorrectly conclude that a resource is unused because it did not * discover a resource reference in an XML file. * <p> * However, there are other issues where the issue can occur in a variety of * files, but the detector can consider each in isolation. For example, the * API checker is affected by both XML files and Java class files (detecting * both layout constructor references in XML layout files as well as code * references in .class files). It doesn't have to analyze both; it is * capable of incrementally analyzing just an XML file, or just a class * file, without considering the other. * <p> * An issue can register additional scope sets that can are adequate * for analyzing the issue, by calling {@link #addAnalysisScope(EnumSet)}. * This method returns true if the given scope matches one or more analysis * scope, or the overall scope. * * @param scope the scope available for analysis * @return true if this issue can be analyzed with the given available scope */ public boolean isAdequate(@NonNull EnumSet<Scope> scope) { if (scope.containsAll(mScope)) { return true; } if (mAnalysisScopes != null) { for (EnumSet<Scope> analysisScope : mAnalysisScopes) { if (mScope.containsAll(analysisScope)) { return true; } } } if (this == IssueRegistry.LINT_ERROR || this == IssueRegistry.PARSER_ERROR) { return true; } return false; } /** * Adds a scope set that can be analyzed independently to uncover this issue. * See the {@link #getAnalysisScopes()} method for a full explanation. * Note that the {@link #getScope()} does not have to be added here; it is * always considered an analysis scope. * * @param scope the additional scope which can analyze this issue independently * @return this, for constructor chaining */ public Issue addAnalysisScope(@Nullable EnumSet<Scope> scope) { if (mAnalysisScopes == null) { mAnalysisScopes = new ArrayList<EnumSet<Scope>>(2); } mAnalysisScopes.add(scope); return this; } /** * Returns the class of the detector to use to find this issue * * @return the class of the detector to use to find this issue */ @NonNull public Class<? extends Detector> getDetectorClass() { return mClass; } @Override public String toString() { return mId; } /** * Converts the given markup text to HTML or text, depending on the. * <p> * This will recognize the following formatting conventions: * <ul> * <li>HTTP urls (http://...) * <li>Sentences immediately surrounded by * will be shown as bold. * <li>Sentences immediately surrounded by ` will be shown using monospace * fonts * </ul> * Furthermore, newlines are converted to br's when converting newlines. * Note: It does not insert {@code <html>} tags around the fragment for HTML output. * <p> * TODO: Consider switching to the restructured text format - * http://docutils.sourceforge.net/docs/user/rst/quickstart.html * * @param text the text to be formatted * @param html whether to convert into HTML or text * @return the corresponding HTML or text properly formatted */ @NonNull public static String convertMarkup(@NonNull String text, boolean html) { StringBuilder sb = new StringBuilder(3 * text.length() / 2); char prev = 0; int flushIndex = 0; int n = text.length(); for (int i = 0; i < n; i++) { char c = text.charAt(i); if ((c == '*' || c == '`' && i < n - 1)) { // Scout ahead for range end if (!Character.isLetterOrDigit(prev) && !Character.isWhitespace(text.charAt(i + 1))) { // Found * or ~ immediately before a letter, and not in the middle of a word // Find end int end = text.indexOf(c, i + 1); if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) { if (i > flushIndex) { appendEscapedText(sb, text, html, flushIndex, i); } if (html) { String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$ sb.append('<').append(tag).append('>'); appendEscapedText(sb, text, html, i + 1, end); sb.append('<').append('/').append(tag).append('>'); } else { appendEscapedText(sb, text, html, i + 1, end); } flushIndex = end + 1; i = flushIndex - 1; // -1: account for the i++ in the loop } } } else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't' && text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) { // Find url end int end = i + HTTP_PREFIX.length(); while (end < n) { char d = text.charAt(end); if (Character.isWhitespace(d)) { break; } end++; } char last = text.charAt(end - 1); if (last == '.' || last == ')' || last == '!') { end--; } if (end > i + HTTP_PREFIX.length()) { if (i > flushIndex) { appendEscapedText(sb, text, html, flushIndex, i); } String url = text.substring(i, end); sb.append("<a href=\""); //$NON-NLS-1$ sb.append(url); sb.append('"').append('>'); sb.append(url); sb.append("</a>"); //$NON-NLS-1$ flushIndex = end; i = flushIndex - 1; // -1: account for the i++ in the loop } } prev = c; } if (flushIndex < n) { appendEscapedText(sb, text, html, flushIndex, n); } return sb.toString(); } static void appendEscapedText(StringBuilder sb, String text, boolean html, int start, int end) { if (html) { for (int i = start; i < end; i++) { char c = text.charAt(i); if (c == '<') { sb.append("<"); //$NON-NLS-1$ } else if (c == '&') { sb.append("&"); //$NON-NLS-1$ } else if (c == '\n') { sb.append("<br/>\n"); } else { if (c > 255) { sb.append("&#"); //$NON-NLS-1$ sb.append(Integer.toString(c)); sb.append(';'); } else { sb.append(c); } } } } else { for (int i = start; i < end; i++) { char c = text.charAt(i); sb.append(c); } } } }