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("<"); //$NON-NLS-1$ 531 } else if (c == '&') { 532 sb.append("&"); //$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