1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.tools.lint.detector.api;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.tools.lint.client.api.Configuration;
22import com.android.tools.lint.client.api.IssueRegistry;
23import com.google.common.annotations.Beta;
24
25import java.util.ArrayList;
26import java.util.Collection;
27import java.util.EnumSet;
28import java.util.List;
29
30
31/**
32 * An issue is a potential bug in an Android application. An issue is discovered
33 * by a {@link Detector}, and has an associated {@link Severity}.
34 * <p>
35 * Issues and detectors are separate classes because a detector can discover
36 * multiple different issues as it's analyzing code, and we want to be able to
37 * different severities for different issues, the ability to suppress one but
38 * not other issues from the same detector, and so on.
39 * <p/>
40 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
41 * to adjust your code for the next tools release.</b>
42 */
43@Beta
44public final class Issue implements Comparable<Issue> {
45    private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$
46
47    private final String mId;
48    private final String mDescription;
49    private final String mExplanation;
50    private final Category mCategory;
51    private final int mPriority;
52    private final Severity mSeverity;
53    private String mMoreInfoUrl;
54    private boolean mEnabledByDefault = true;
55    private final EnumSet<Scope> mScope;
56    private List<EnumSet<Scope>> mAnalysisScopes;
57    private final Class<? extends Detector> mClass;
58
59    // Use factory methods
60    private Issue(
61            @NonNull String id,
62            @NonNull String description,
63            @NonNull String explanation,
64            @NonNull Category category,
65            int priority,
66            @NonNull Severity severity,
67            @NonNull Class<? extends Detector> detectorClass,
68            @NonNull EnumSet<Scope> scope) {
69        super();
70        mId = id;
71        mDescription = description;
72        mExplanation = explanation;
73        mCategory = category;
74        mPriority = priority;
75        mSeverity = severity;
76        mClass = detectorClass;
77        mScope = scope;
78    }
79
80    /**
81     * Creates a new issue
82     *
83     * @param id the fixed id of the issue
84     * @param description the quick summary of the issue (one line)
85     * @param explanation a full explanation of the issue, with suggestions for
86     *            how to fix it
87     * @param category the associated category, if any
88     * @param priority the priority, a number from 1 to 10 with 10 being most
89     *            important/severe
90     * @param severity the default severity of the issue
91     * @param detectorClass the class of the detector to find this issue
92     * @param scope the scope of files required to analyze this issue
93     * @return a new {@link Issue}
94     */
95    @NonNull
96    public static Issue create(
97            @NonNull String id,
98            @NonNull String description,
99            @NonNull String explanation,
100            @NonNull Category category,
101            int priority,
102            @NonNull Severity severity,
103            @NonNull Class<? extends Detector> detectorClass,
104            @NonNull EnumSet<Scope> scope) {
105        return new Issue(id, description, explanation, category, priority, severity,
106                detectorClass, scope);
107    }
108
109    /**
110     * Returns the unique id of this issue. These should not change over time
111     * since they are used to persist the names of issues suppressed by the user
112     * etc. It is typically a single camel-cased word.
113     *
114     * @return the associated fixed id, never null and always unique
115     */
116    @NonNull
117    public String getId() {
118        return mId;
119    }
120
121    /**
122     * Briefly (one line) describes the kinds of checks performed by this rule
123     *
124     * @return a quick summary of the issue, never null
125     */
126    @NonNull
127    public String getDescription() {
128        return mDescription;
129    }
130
131    /**
132     * Describes the error found by this rule, e.g.
133     * "Buttons must define contentDescriptions". Preferably the explanation
134     * should also contain a description of how the problem should be solved.
135     * Additional info can be provided via {@link #getMoreInfo()}.
136     * <p>
137     * Note that the text may contain some simple markup, such as *'s around sentences
138     * for bold text, and back quotes (`) for code fragments. You can obtain
139     * the text without this markup by calling {@link #getExplanationAsSimpleText()},
140     * and you can obtain the text as annotated HTML by calling
141     * {@link #getExplanationAsHtml()}.
142     *
143     * @return an explanation of the issue, never null.
144     */
145    @NonNull
146    public String getExplanation() {
147        return mExplanation;
148    }
149
150    /**
151     * Like {@link #getExplanation()}, but returns the text as properly escaped
152     * and marked up HTML, where http URLs are linked, where words with asterisks
153     * such as *this* are shown in bold, etc.
154     *
155     * @return the explanation of the issue, never null
156     */
157    @NonNull
158    public String getExplanationAsHtml() {
159        return convertMarkup(mExplanation, true /* html */);
160    }
161
162    /**
163     * Like {@link #getExplanation()}, but returns the text as properly escaped
164     * and marked up HTML, where http URLs are linked, where words with asterisks
165     * such as *this* are shown in bold, etc.
166     *
167     * @return the explanation of the issue, never null
168     */
169    @NonNull
170    public String getExplanationAsSimpleText() {
171        return convertMarkup(mExplanation, false /* not html = text */);
172    }
173
174    /**
175     * The primary category of the issue
176     *
177     * @return the primary category of the issue, never null
178     */
179    @NonNull
180    public Category getCategory() {
181        return mCategory;
182    }
183
184    /**
185     * Returns a priority, in the range 1-10, with 10 being the most severe and
186     * 1 the least
187     *
188     * @return a priority from 1 to 10
189     */
190    public int getPriority() {
191        return mPriority;
192    }
193
194    /**
195     * Returns the default severity of the issues found by this detector (some
196     * tools may allow the user to specify custom severities for detectors).
197     * <p>
198     * Note that even though the normal way for an issue to be disabled is for
199     * the {@link Configuration} to return {@link Severity#IGNORE}, there is a
200     * {@link #isEnabledByDefault()} method which can be used to turn off issues
201     * by default. This is done rather than just having the severity as the only
202     * attribute on the issue such that an issue can be configured with an
203     * appropriate severity (such as {@link Severity#ERROR}) even when issues
204     * are disabled by default for example because they are experimental or not
205     * yet stable.
206     *
207     * @return the severity of the issues found by this detector
208     */
209    @NonNull
210    public Severity getDefaultSeverity() {
211        return mSeverity;
212    }
213
214    /**
215     * Returns a link (a URL string) to more information, or null
216     *
217     * @return a link to more information, or null
218     */
219    @Nullable
220    public String getMoreInfo() {
221        return mMoreInfoUrl;
222    }
223
224    /**
225     * Returns whether this issue should be enabled by default, unless the user
226     * has explicitly disabled it.
227     *
228     * @return true if this issue should be enabled by default
229     */
230    public boolean isEnabledByDefault() {
231        return mEnabledByDefault;
232    }
233
234    /**
235     * Returns the scope required to analyze the code to detect this issue.
236     * This is determined by the detectors which reports the issue.
237     *
238     * @return the required scope
239     */
240    @NonNull
241    public EnumSet<Scope> getScope() {
242        return mScope;
243    }
244
245    /**
246     * Sorts the detectors alphabetically by id. This is intended to make it
247     * convenient to store settings for detectors in a fixed order. It is not
248     * intended as the order to be shown to the user; for that, a tool embedding
249     * lint might consider the priorities, categories, severities etc of the
250     * various detectors.
251     *
252     * @param other the {@link Issue} to compare this issue to
253     */
254    @Override
255    public int compareTo(Issue other) {
256        return getId().compareTo(other.getId());
257    }
258
259    /**
260     * Sets a more info URL string
261     *
262     * @param moreInfoUrl url string
263     * @return this, for constructor chaining
264     */
265    @NonNull
266    public Issue setMoreInfo(@NonNull String moreInfoUrl) {
267        mMoreInfoUrl = moreInfoUrl;
268        return this;
269    }
270
271    /**
272     * Sets whether this issue is enabled by default.
273     *
274     * @param enabledByDefault whether the issue should be enabled by default
275     * @return this, for constructor chaining
276     */
277    @NonNull
278    public Issue setEnabledByDefault(boolean enabledByDefault) {
279        mEnabledByDefault = enabledByDefault;
280        return this;
281    }
282
283    /**
284     * Returns the sets of scopes required to analyze this issue, or null if all
285     * scopes named by {@link Issue#getScope()} are necessary. Note that only
286     * <b>one</b> match out of this collection is required, not all, and that
287     * the scope set returned by {@link #getScope()} does not have to be returned
288     * by this method, but is always implied to be included.
289     * <p>
290     * The scopes returned by {@link Issue#getScope()} list all the various
291     * scopes that are <b>affected</b> by this issue, meaning the detector
292     * should consider it. Frequently, the detector must analyze all these
293     * scopes in order to properly decide whether an issue is found. For
294     * example, the unused resource detector needs to consider both the XML
295     * resource files and the Java source files in order to decide if a resource
296     * is unused. If it analyzes just the Java files for example, it might
297     * incorrectly conclude that a resource is unused because it did not
298     * discover a resource reference in an XML file.
299     * <p>
300     * However, there are other issues where the issue can occur in a variety of
301     * files, but the detector can consider each in isolation. For example, the
302     * API checker is affected by both XML files and Java class files (detecting
303     * both layout constructor references in XML layout files as well as code
304     * references in .class files). It doesn't have to analyze both; it is
305     * capable of incrementally analyzing just an XML file, or just a class
306     * file, without considering the other.
307     * <p>
308     * The required scope list provides a list of scope sets that can be used to
309     * analyze this issue. For each scope set, all the scopes must be matched by
310     * the incremental analysis, but any one of the scope sets can be analyzed
311     * in isolation.
312     * <p>
313     * The required scope list is not required to include the full scope set
314     * returned by {@link #getScope()}; that set is always assumed to be
315     * included.
316     * <p>
317     * NOTE: You would normally call {@link #isAdequate(EnumSet)} rather
318     * than calling this method directly.
319     *
320     * @return a list of required scopes, or null.
321     */
322    @Nullable
323    public Collection<EnumSet<Scope>> getAnalysisScopes() {
324        return mAnalysisScopes;
325    }
326
327    /**
328     * Sets the collection of scopes that are allowed to be analyzed independently.
329     * See the {@link #getAnalysisScopes()} method for a full explanation.
330     * Note that you usually want to just call {@link #addAnalysisScope(EnumSet)}
331     * instead of constructing a list up front and passing it in here. This
332     * method exists primarily such that commonly used share sets of analysis
333     * scopes can be reused and set directly.
334     *
335     * @param required the collection of scopes
336     * @return this, for constructor chaining
337     */
338    public Issue setAnalysisScopes(@Nullable List<EnumSet<Scope>> required) {
339        mAnalysisScopes = required;
340
341        return this;
342    }
343
344    /**
345     * Returns true if the given scope is adequate for analyzing this issue.
346     * This looks through the analysis scopes (see
347     * {@link #addAnalysisScope(EnumSet)}) and if the scope passed in fully
348     * covers at least one of them, or if it covers the scope of the issue
349     * itself (see {@link #getScope()}, which should be a superset of all the
350     * analysis scopes) returns true.
351     * <p>
352     * The scope set returned by {@link Issue#getScope()} lists all the various
353     * scopes that are <b>affected</b> by this issue, meaning the detector
354     * should consider it. Frequently, the detector must analyze all these
355     * scopes in order to properly decide whether an issue is found. For
356     * example, the unused resource detector needs to consider both the XML
357     * resource files and the Java source files in order to decide if a resource
358     * is unused. If it analyzes just the Java files for example, it might
359     * incorrectly conclude that a resource is unused because it did not
360     * discover a resource reference in an XML file.
361     * <p>
362     * However, there are other issues where the issue can occur in a variety of
363     * files, but the detector can consider each in isolation. For example, the
364     * API checker is affected by both XML files and Java class files (detecting
365     * both layout constructor references in XML layout files as well as code
366     * references in .class files). It doesn't have to analyze both; it is
367     * capable of incrementally analyzing just an XML file, or just a class
368     * file, without considering the other.
369     * <p>
370     * An issue can register additional scope sets that can are adequate
371     * for analyzing the issue, by calling {@link #addAnalysisScope(EnumSet)}.
372     * This method returns true if the given scope matches one or more analysis
373     * scope, or the overall scope.
374     *
375     * @param scope the scope available for analysis
376     * @return true if this issue can be analyzed with the given available scope
377     */
378    public boolean isAdequate(@NonNull EnumSet<Scope> scope) {
379        if (scope.containsAll(mScope)) {
380            return true;
381        }
382
383        if (mAnalysisScopes != null) {
384            for (EnumSet<Scope> analysisScope : mAnalysisScopes) {
385                if (mScope.containsAll(analysisScope)) {
386                    return true;
387                }
388            }
389        }
390
391        if (this == IssueRegistry.LINT_ERROR || this == IssueRegistry.PARSER_ERROR) {
392            return true;
393        }
394
395        return false;
396    }
397
398    /**
399     * Adds a scope set that can be analyzed independently to uncover this issue.
400     * See the {@link #getAnalysisScopes()} method for a full explanation.
401     * Note that the {@link #getScope()} does not have to be added here; it is
402     * always considered an analysis scope.
403     *
404     * @param scope the additional scope which can analyze this issue independently
405     * @return this, for constructor chaining
406     */
407    public Issue addAnalysisScope(@Nullable EnumSet<Scope> scope) {
408        if (mAnalysisScopes == null) {
409            mAnalysisScopes = new ArrayList<EnumSet<Scope>>(2);
410        }
411        mAnalysisScopes.add(scope);
412
413        return this;
414    }
415
416    /**
417     * Returns the class of the detector to use to find this issue
418     *
419     * @return the class of the detector to use to find this issue
420     */
421    @NonNull
422    public Class<? extends Detector> getDetectorClass() {
423        return mClass;
424    }
425
426    @Override
427    public String toString() {
428        return mId;
429    }
430
431    /**
432     * Converts the given markup text to HTML or text, depending on the.
433     * <p>
434     * This will recognize the following formatting conventions:
435     * <ul>
436     * <li>HTTP urls (http://...)
437     * <li>Sentences immediately surrounded by * will be shown as bold.
438     * <li>Sentences immediately surrounded by ` will be shown using monospace
439     * fonts
440     * </ul>
441     * Furthermore, newlines are converted to br's when converting newlines.
442     * Note: It does not insert {@code <html>} tags around the fragment for HTML output.
443     * <p>
444     * TODO: Consider switching to the restructured text format -
445     *  http://docutils.sourceforge.net/docs/user/rst/quickstart.html
446     *
447     * @param text the text to be formatted
448     * @param html whether to convert into HTML or text
449     * @return the corresponding HTML or text properly formatted
450     */
451    @NonNull
452    public static String convertMarkup(@NonNull String text, boolean html) {
453        StringBuilder sb = new StringBuilder(3 * text.length() / 2);
454
455        char prev = 0;
456        int flushIndex = 0;
457        int n = text.length();
458        for (int i = 0; i < n; i++) {
459            char c = text.charAt(i);
460            if ((c == '*' || c == '`' && i < n - 1)) {
461                // Scout ahead for range end
462                if (!Character.isLetterOrDigit(prev)
463                        && !Character.isWhitespace(text.charAt(i + 1))) {
464                    // Found * or ~ immediately before a letter, and not in the middle of a word
465                    // Find end
466                    int end = text.indexOf(c, i + 1);
467                    if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
468                        if (i > flushIndex) {
469                            appendEscapedText(sb, text, html, flushIndex, i);
470                        }
471                        if (html) {
472                            String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$
473                            sb.append('<').append(tag).append('>');
474                            appendEscapedText(sb, text, html, i + 1, end);
475                            sb.append('<').append('/').append(tag).append('>');
476                        } else {
477                            appendEscapedText(sb, text, html, i + 1, end);
478                        }
479                        flushIndex = end + 1;
480                        i = flushIndex - 1; // -1: account for the i++ in the loop
481                    }
482                }
483            } else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't'
484                    && text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) {
485                // Find url end
486                int end = i + HTTP_PREFIX.length();
487                while (end < n) {
488                    char d = text.charAt(end);
489                    if (Character.isWhitespace(d)) {
490                        break;
491                    }
492                    end++;
493                }
494                char last = text.charAt(end - 1);
495                if (last == '.' || last == ')' || last == '!') {
496                    end--;
497                }
498                if (end > i + HTTP_PREFIX.length()) {
499                    if (i > flushIndex) {
500                        appendEscapedText(sb, text, html, flushIndex, i);
501                    }
502
503                    String url = text.substring(i, end);
504                    sb.append("<a href=\"");        //$NON-NLS-1$
505                    sb.append(url);
506                    sb.append('"').append('>');
507                    sb.append(url);
508                    sb.append("</a>");              //$NON-NLS-1$
509
510                    flushIndex = end;
511                    i = flushIndex - 1; // -1: account for the i++ in the loop
512                }
513            }
514            prev = c;
515        }
516
517        if (flushIndex < n) {
518            appendEscapedText(sb, text, html, flushIndex, n);
519        }
520
521        return sb.toString();
522    }
523
524    static void appendEscapedText(StringBuilder sb, String text, boolean html,
525            int start, int end) {
526        if (html) {
527            for (int i = start; i < end; i++) {
528                char c = text.charAt(i);
529                if (c == '<') {
530                    sb.append("&lt;");                                   //$NON-NLS-1$
531                } else if (c == '&') {
532                    sb.append("&amp;");                                  //$NON-NLS-1$
533                } else if (c == '\n') {
534                    sb.append("<br/>\n");
535                } else {
536                    if (c > 255) {
537                        sb.append("&#");                                 //$NON-NLS-1$
538                        sb.append(Integer.toString(c));
539                        sb.append(';');
540                    } else {
541                        sb.append(c);
542                    }
543                }
544            }
545        } else {
546            for (int i = start; i < end; i++) {
547                char c = text.charAt(i);
548                sb.append(c);
549            }
550        }
551    }
552}
553