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 17 package com.android.tools.lint; 18 19 import static com.android.tools.lint.client.api.IssueRegistry.LINT_ERROR; 20 import static com.android.tools.lint.client.api.IssueRegistry.PARSER_ERROR; 21 import static com.android.tools.lint.detector.api.LintConstants.DOT_XML; 22 import static com.android.tools.lint.detector.api.LintUtils.endsWith; 23 24 import com.android.annotations.Nullable; 25 import com.android.tools.lint.checks.BuiltinIssueRegistry; 26 import com.android.tools.lint.client.api.Configuration; 27 import com.android.tools.lint.client.api.DefaultConfiguration; 28 import com.android.tools.lint.client.api.IDomParser; 29 import com.android.tools.lint.client.api.IJavaParser; 30 import com.android.tools.lint.client.api.IssueRegistry; 31 import com.android.tools.lint.client.api.LintClient; 32 import com.android.tools.lint.client.api.LintDriver; 33 import com.android.tools.lint.client.api.LintListener; 34 import com.android.tools.lint.detector.api.Category; 35 import com.android.tools.lint.detector.api.Context; 36 import com.android.tools.lint.detector.api.Issue; 37 import com.android.tools.lint.detector.api.LintUtils; 38 import com.android.tools.lint.detector.api.Location; 39 import com.android.tools.lint.detector.api.Position; 40 import com.android.tools.lint.detector.api.Project; 41 import com.android.tools.lint.detector.api.Severity; 42 import com.google.common.io.Closeables; 43 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.io.IOException; 47 import java.io.PrintStream; 48 import java.io.PrintWriter; 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.Comparator; 52 import java.util.HashMap; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.Map; 56 import java.util.Properties; 57 import java.util.Set; 58 59 /** 60 * Command line driver for the lint framework 61 */ 62 public class Main extends LintClient { 63 static final int MAX_LINE_WIDTH = 78; 64 private static final String ARG_ENABLE = "--enable"; //$NON-NLS-1$ 65 private static final String ARG_DISABLE = "--disable"; //$NON-NLS-1$ 66 private static final String ARG_CHECK = "--check"; //$NON-NLS-1$ 67 private static final String ARG_IGNORE = "--ignore"; //$NON-NLS-1$ 68 private static final String ARG_LISTIDS = "--list"; //$NON-NLS-1$ 69 private static final String ARG_SHOW = "--show"; //$NON-NLS-1$ 70 private static final String ARG_QUIET = "--quiet"; //$NON-NLS-1$ 71 private static final String ARG_FULLPATH = "--fullpath"; //$NON-NLS-1$ 72 private static final String ARG_SHOWALL = "--showall"; //$NON-NLS-1$ 73 private static final String ARG_HELP = "--help"; //$NON-NLS-1$ 74 private static final String ARG_NOLINES = "--nolines"; //$NON-NLS-1$ 75 private static final String ARG_HTML = "--html"; //$NON-NLS-1$ 76 private static final String ARG_SIMPLEHTML = "--simplehtml"; //$NON-NLS-1$ 77 private static final String ARG_XML = "--xml"; //$NON-NLS-1$ 78 private static final String ARG_CONFIG = "--config"; //$NON-NLS-1$ 79 private static final String ARG_URL = "--url"; //$NON-NLS-1$ 80 private static final String ARG_VERSION = "--version"; //$NON-NLS-1$ 81 private static final String ARG_EXITCODE = "--exitcode"; //$NON-NLS-1$ 82 83 private static final String ARG_NOWARN2 = "--nowarn"; //$NON-NLS-1$ 84 // GCC style flag names for options 85 private static final String ARG_NOWARN1 = "-w"; //$NON-NLS-1$ 86 private static final String ARG_WARNALL = "-Wall"; //$NON-NLS-1$ 87 private static final String ARG_ALLERROR = "-Werror"; //$NON-NLS-1$ 88 89 private static final String VALUE_NONE = "none"; //$NON-NLS-1$ 90 91 private static final String PROP_WORK_DIR = "com.android.tools.lint.workdir"; //$NON-NLS-1$ 92 93 private static final int ERRNO_ERRORS = 1; 94 private static final int ERRNO_USAGE = 2; 95 private static final int ERRNO_EXISTS = 3; 96 private static final int ERRNO_HELP = 4; 97 private static final int ERRNO_INVALIDARGS = 5; 98 99 private List<Warning> mWarnings = new ArrayList<Warning>(); 100 private Set<String> mSuppress = new HashSet<String>(); 101 private Set<String> mEnabled = new HashSet<String>(); 102 /** If non-null, only run the specified checks (possibly modified by enable/disables) */ 103 private Set<String> mCheck = null; 104 private boolean mHasErrors; 105 private boolean mSetExitCode; 106 private boolean mFullPath; 107 private int mErrorCount; 108 private int mWarningCount; 109 private boolean mShowLines = true; 110 private Reporter mReporter; 111 private boolean mQuiet; 112 private boolean mWarnAll; 113 private boolean mNoWarnings; 114 private boolean mAllErrors; 115 116 private Configuration mDefaultConfiguration; 117 private IssueRegistry mRegistry; 118 private LintDriver mDriver; 119 private boolean mShowAll; 120 121 /** Creates a CLI driver */ Main()122 public Main() { 123 } 124 125 /** 126 * Runs the static analysis command line driver 127 * 128 * @param args program arguments 129 */ main(String[] args)130 public static void main(String[] args) { 131 new Main().run(args); 132 } 133 134 /** 135 * Runs the static analysis command line driver 136 * 137 * @param args program arguments 138 */ run(String[] args)139 private void run(String[] args) { 140 if (args.length < 1) { 141 printUsage(System.err); 142 System.exit(ERRNO_USAGE); 143 } 144 145 IssueRegistry registry = mRegistry = new BuiltinIssueRegistry(); 146 147 // Mapping from file path prefix to URL. Applies only to HTML reports 148 String urlMap = null; 149 150 List<File> files = new ArrayList<File>(); 151 for (int index = 0; index < args.length; index++) { 152 String arg = args[index]; 153 154 if (arg.equals(ARG_HELP) 155 || arg.equals("-h") || arg.equals("-?")) { //$NON-NLS-1$ //$NON-NLS-2$ 156 if (index < args.length - 1) { 157 String topic = args[index + 1]; 158 if (topic.equals("suppress") || topic.equals("ignore")) { 159 printHelpTopicSuppress(); 160 System.exit(ERRNO_HELP); 161 } else { 162 System.err.println(String.format("Unknown help topic \"%1$s\"", topic)); 163 System.exit(ERRNO_INVALIDARGS); 164 } 165 } 166 printUsage(System.out); 167 System.exit(ERRNO_HELP); 168 } else if (arg.equals(ARG_LISTIDS)) { 169 // Did the user provide a category list? 170 if (index < args.length - 1 && !args[index + 1].startsWith("-")) { //$NON-NLS-1$ 171 String[] ids = args[++index].split(","); 172 for (String id : ids) { 173 if (registry.isCategoryName(id)) { 174 // List all issues with the given category 175 String category = id; 176 for (Issue issue : registry.getIssues()) { 177 // Check prefix such that filtering on the "Usability" category 178 // will match issue category "Usability:Icons" etc. 179 if (issue.getCategory().getName().startsWith(category) || 180 issue.getCategory().getFullName().startsWith(category)) { 181 listIssue(System.out, issue); 182 } 183 } 184 } else { 185 System.err.println("Invalid category \"" + id + "\".\n"); 186 displayValidIds(registry, System.err); 187 System.exit(ERRNO_INVALIDARGS); 188 } 189 } 190 } else { 191 displayValidIds(registry, System.out); 192 } 193 System.exit(0); 194 } else if (arg.equals(ARG_SHOW)) { 195 // Show specific issues? 196 if (index < args.length - 1 && !args[index + 1].startsWith("-")) { //$NON-NLS-1$ 197 String[] ids = args[++index].split(","); 198 for (String id : ids) { 199 if (registry.isCategoryName(id)) { 200 // Show all issues in the given category 201 String category = id; 202 for (Issue issue : registry.getIssues()) { 203 // Check prefix such that filtering on the "Usability" category 204 // will match issue category "Usability:Icons" etc. 205 if (issue.getCategory().getName().startsWith(category) || 206 issue.getCategory().getFullName().startsWith(category)) { 207 describeIssue(issue); 208 System.out.println(); 209 } 210 } 211 } else if (registry.isIssueId(id)) { 212 describeIssue(registry.getIssue(id)); 213 System.out.println(); 214 } else { 215 System.err.println("Invalid id or category \"" + id + "\".\n"); 216 displayValidIds(registry, System.err); 217 System.exit(ERRNO_INVALIDARGS); 218 } 219 } 220 } else { 221 showIssues(registry); 222 } 223 System.exit(0); 224 } else if (arg.equals(ARG_FULLPATH) 225 || arg.equals(ARG_FULLPATH + "s")) { // allow "--fullpaths" too 226 mFullPath = true; 227 } else if (arg.equals(ARG_SHOWALL)) { 228 mShowAll = true; 229 } else if (arg.equals(ARG_QUIET) || arg.equals("-q")) { 230 mQuiet = true; 231 } else if (arg.equals(ARG_NOLINES)) { 232 mShowLines = false; 233 } else if (arg.equals(ARG_EXITCODE)) { 234 mSetExitCode = true; 235 } else if (arg.equals(ARG_VERSION)) { 236 printVersion(); 237 System.exit(0); 238 } else if (arg.equals(ARG_URL)) { 239 if (index == args.length - 1) { 240 System.err.println("Missing URL mapping string"); 241 System.exit(ERRNO_INVALIDARGS); 242 } 243 String map = args[++index]; 244 // Allow repeated usage of the argument instead of just comma list 245 if (urlMap != null) { 246 urlMap = urlMap + ',' + map; 247 } else { 248 urlMap = map; 249 } 250 } else if (arg.equals(ARG_CONFIG)) { 251 if (index == args.length - 1 || !endsWith(args[index + 1], DOT_XML)) { 252 System.err.println("Missing XML configuration file argument"); 253 System.exit(ERRNO_INVALIDARGS); 254 } 255 File file = getInArgumentPath(args[++index]); 256 if (!file.exists()) { 257 System.err.println(file.getAbsolutePath() + " does not exist"); 258 System.exit(ERRNO_INVALIDARGS); 259 } 260 mDefaultConfiguration = new CliConfiguration(file); 261 } else if (arg.equals(ARG_HTML) || arg.equals(ARG_SIMPLEHTML)) { 262 if (index == args.length - 1) { 263 System.err.println("Missing HTML output file name"); 264 System.exit(ERRNO_INVALIDARGS); 265 } 266 File output = getOutArgumentPath(args[++index]); 267 // Get an absolute path such that we can ask its parent directory for 268 // write permission etc. 269 output = output.getAbsoluteFile(); 270 if (output.isDirectory() || 271 (!output.exists() && output.getName().indexOf('.') == -1)) { 272 if (!output.exists()) { 273 boolean mkdirs = output.mkdirs(); 274 if (!mkdirs) { 275 log(null, "Could not create output directory %1$s", output); 276 System.exit(ERRNO_EXISTS); 277 } 278 } 279 try { 280 MultiProjectHtmlReporter reporter = 281 new MultiProjectHtmlReporter(this, output); 282 if (arg.equals(ARG_SIMPLEHTML)) { 283 reporter.setSimpleFormat(true); 284 } 285 mReporter = reporter; 286 } catch (IOException e) { 287 log(e, null); 288 System.exit(ERRNO_INVALIDARGS); 289 } 290 continue; 291 } 292 if (output.exists()) { 293 boolean delete = output.delete(); 294 if (!delete) { 295 System.err.println("Could not delete old " + output); 296 System.exit(ERRNO_EXISTS); 297 } 298 } 299 if (output.getParentFile() != null && !output.getParentFile().canWrite()) { 300 System.err.println("Cannot write HTML output file " + output); 301 System.exit(ERRNO_EXISTS); 302 } 303 try { 304 HtmlReporter htmlReporter = new HtmlReporter(this, output); 305 if (arg.equals(ARG_SIMPLEHTML)) { 306 htmlReporter.setSimpleFormat(true); 307 } 308 mReporter = htmlReporter; 309 } catch (IOException e) { 310 log(e, null); 311 System.exit(ERRNO_INVALIDARGS); 312 } 313 } else if (arg.equals(ARG_XML)) { 314 if (index == args.length - 1) { 315 System.err.println("Missing XML output file name"); 316 System.exit(ERRNO_INVALIDARGS); 317 } 318 File output = getOutArgumentPath(args[++index]); 319 if (output.exists()) { 320 boolean delete = output.delete(); 321 if (!delete) { 322 System.err.println("Could not delete old " + output); 323 System.exit(ERRNO_EXISTS); 324 } 325 } 326 if (output.canWrite()) { 327 System.err.println("Cannot write XML output file " + output); 328 System.exit(ERRNO_EXISTS); 329 } 330 try { 331 mReporter = new XmlReporter(this, output); 332 } catch (IOException e) { 333 log(e, null); 334 System.exit(ERRNO_INVALIDARGS); 335 } 336 } else if (arg.equals(ARG_DISABLE) || arg.equals(ARG_IGNORE)) { 337 if (index == args.length - 1) { 338 System.err.println("Missing categories or id's to disable"); 339 System.exit(ERRNO_INVALIDARGS); 340 } 341 String[] ids = args[++index].split(","); 342 for (String id : ids) { 343 if (registry.isCategoryName(id)) { 344 // Suppress all issues with the given category 345 String category = id; 346 for (Issue issue : registry.getIssues()) { 347 // Check prefix such that filtering on the "Usability" category 348 // will match issue category "Usability:Icons" etc. 349 if (issue.getCategory().getName().startsWith(category) || 350 issue.getCategory().getFullName().startsWith(category)) { 351 mSuppress.add(issue.getId()); 352 } 353 } 354 } else if (!registry.isIssueId(id)) { 355 System.err.println("Invalid id or category \"" + id + "\".\n"); 356 displayValidIds(registry, System.err); 357 System.exit(ERRNO_INVALIDARGS); 358 } else { 359 mSuppress.add(id); 360 } 361 } 362 } else if (arg.equals(ARG_ENABLE)) { 363 if (index == args.length - 1) { 364 System.err.println("Missing categories or id's to enable"); 365 System.exit(ERRNO_INVALIDARGS); 366 } 367 String[] ids = args[++index].split(","); 368 for (String id : ids) { 369 if (registry.isCategoryName(id)) { 370 // Enable all issues with the given category 371 String category = id; 372 for (Issue issue : registry.getIssues()) { 373 if (issue.getCategory().getName().startsWith(category) || 374 issue.getCategory().getFullName().startsWith(category)) { 375 mEnabled.add(issue.getId()); 376 } 377 } 378 } else if (!registry.isIssueId(id)) { 379 System.err.println("Invalid id or category \"" + id + "\".\n"); 380 displayValidIds(registry, System.err); 381 System.exit(ERRNO_INVALIDARGS); 382 } else { 383 mEnabled.add(id); 384 } 385 } 386 } else if (arg.equals(ARG_CHECK)) { 387 if (index == args.length - 1) { 388 System.err.println("Missing categories or id's to check"); 389 System.exit(ERRNO_INVALIDARGS); 390 } 391 mCheck = new HashSet<String>(); 392 String[] ids = args[++index].split(","); 393 for (String id : ids) { 394 if (registry.isCategoryName(id)) { 395 // Check all issues with the given category 396 String category = id; 397 for (Issue issue : registry.getIssues()) { 398 // Check prefix such that filtering on the "Usability" category 399 // will match issue category "Usability:Icons" etc. 400 if (issue.getCategory().getName().startsWith(category) || 401 issue.getCategory().getFullName().startsWith(category)) { 402 mCheck.add(issue.getId()); 403 } 404 } 405 } else if (!registry.isIssueId(id)) { 406 System.err.println("Invalid id or category \"" + id + "\".\n"); 407 displayValidIds(registry, System.err); 408 System.exit(ERRNO_INVALIDARGS); 409 } else { 410 mCheck.add(id); 411 } 412 } 413 } else if (arg.equals(ARG_NOWARN1) || arg.equals(ARG_NOWARN2)) { 414 mNoWarnings = true; 415 } else if (arg.equals(ARG_WARNALL)) { 416 mWarnAll = true; 417 } else if (arg.equals(ARG_ALLERROR)) { 418 mAllErrors = true; 419 } else if (arg.startsWith("--")) { 420 System.err.println("Invalid argument " + arg + "\n"); 421 printUsage(System.err); 422 System.exit(ERRNO_INVALIDARGS); 423 } else { 424 String filename = arg; 425 File file = getInArgumentPath(filename); 426 427 if (!file.exists()) { 428 System.err.println(String.format("%1$s does not exist.", filename)); 429 System.exit(ERRNO_EXISTS); 430 } 431 files.add(file); 432 } 433 } 434 435 if (files.size() == 0) { 436 System.err.println("No files to analyze."); 437 System.exit(ERRNO_INVALIDARGS); 438 } 439 440 if (mReporter == null) { 441 if (urlMap != null) { 442 System.err.println(String.format( 443 "Warning: The %1$s option only applies to HTML reports (%2$s)", 444 ARG_URL, ARG_HTML)); 445 } 446 447 mReporter = new TextReporter(this, new PrintWriter(System.out, true)); 448 } else { 449 if (urlMap == null) { 450 // By default just map from /foo to file:///foo 451 // TODO: Find out if we need file:// on Windows. 452 urlMap = "=file://"; //$NON-NLS-1$ 453 } else { 454 if (!mReporter.isSimpleFormat()) { 455 mReporter.setBundleResources(true); 456 } 457 } 458 459 if (!urlMap.equals(VALUE_NONE)) { 460 Map<String, String> map = new HashMap<String, String>(); 461 String[] replace = urlMap.split(","); //$NON-NLS-1$ 462 for (String s : replace) { 463 // Allow ='s in the suffix part 464 int index = s.indexOf('='); 465 if (index == -1) { 466 System.err.println( 467 "The URL map argument must be of the form 'path_prefix=url_prefix'"); 468 System.exit(ERRNO_INVALIDARGS); 469 } 470 String key = s.substring(0, index); 471 String value = s.substring(index + 1); 472 map.put(key, value); 473 } 474 mReporter.setUrlMap(map); 475 } 476 } 477 478 mDriver = new LintDriver(registry, this); 479 480 mDriver.setAbbreviating(!mShowAll); 481 if (!mQuiet) { 482 mDriver.addLintListener(new ProgressPrinter()); 483 } 484 485 mDriver.analyze(files, null /* scope */); 486 487 Collections.sort(mWarnings); 488 489 try { 490 mReporter.write(mErrorCount, mWarningCount, mWarnings); 491 } catch (IOException e) { 492 log(e, null); 493 System.exit(ERRNO_INVALIDARGS); 494 } 495 496 System.exit(mSetExitCode ? (mHasErrors ? ERRNO_ERRORS : 0) : 0); 497 } 498 499 /** 500 * Converts a relative or absolute command-line argument into an input file. 501 * 502 * @param filename The filename given as a command-line argument. 503 * @return A File matching filename, either absolute or relative to lint.workdir if defined. 504 */ getInArgumentPath(String filename)505 private File getInArgumentPath(String filename) { 506 File file = new File(filename); 507 508 if (!file.isAbsolute()) { 509 File workDir = getLintWorkDir(); 510 if (workDir != null) { 511 File file2 = new File(workDir, filename); 512 if (file2.exists()) { 513 try { 514 file = file2.getCanonicalFile(); 515 } catch (IOException e) { 516 file = file2; 517 } 518 } 519 } 520 } 521 return file; 522 } 523 524 /** 525 * Converts a relative or absolute command-line argument into an output file. 526 * <p/> 527 * The difference with {@code getInArgumentPath} is that we can't check whether the 528 * a relative path turned into an absolute compared to lint.workdir actually exists. 529 * 530 * @param filename The filename given as a command-line argument. 531 * @return A File matching filename, either absolute or relative to lint.workdir if defined. 532 */ getOutArgumentPath(String filename)533 private File getOutArgumentPath(String filename) { 534 File file = new File(filename); 535 536 if (!file.isAbsolute()) { 537 File workDir = getLintWorkDir(); 538 if (workDir != null) { 539 File file2 = new File(workDir, filename); 540 try { 541 file = file2.getCanonicalFile(); 542 } catch (IOException e) { 543 file = file2; 544 } 545 } 546 } 547 return file; 548 } 549 550 551 /** 552 * Returns the File corresponding to the system property or the environment variable 553 * for {@link #PROP_WORK_DIR}. 554 * This property is typically set by the SDK/tools/lint[.bat] wrapper. 555 * It denotes the path where the command-line client was originally invoked from 556 * and can be used to convert relative input/output paths. 557 * 558 * @return A new File corresponding to {@link #PROP_WORK_DIR} or null. 559 */ 560 @Nullable getLintWorkDir()561 private File getLintWorkDir() { 562 // First check the Java properties (e.g. set using "java -jar ... -Dname=value") 563 String path = System.getProperty(PROP_WORK_DIR); 564 if (path == null || path.length() == 0) { 565 // If not found, check environment variables. 566 path = System.getenv(PROP_WORK_DIR); 567 } 568 if (path != null && path.length() > 0) { 569 return new File(path); 570 } 571 return null; 572 } 573 printHelpTopicSuppress()574 private void printHelpTopicSuppress() { 575 System.out.println(wrap(getSuppressHelp())); 576 } 577 getSuppressHelp()578 static String getSuppressHelp() { 579 return 580 "Lint errors can be suppressed in a variety of ways:\n" + 581 "\n" + 582 "1. With a @SuppressLint annotation in the Java code\n" + 583 "2. With a tools:ignore attribute in the XML file\n" + 584 "3. With a lint.xml configuration file in the project\n" + 585 "4. With a lint.xml configuration file passed to lint " + 586 "via the " + ARG_CONFIG + " flag\n" + 587 "5. With the " + ARG_IGNORE + " flag passed to lint.\n" + 588 "\n" + 589 "To suppress a lint warning with an annotation, add " + 590 "a @SuppressLint(\"id\") annotation on the class, method " + 591 "or variable declaration closest to the warning instance " + 592 "you want to disable. The id can be one or more issue " + 593 "id's, such as \"UnusedResources\" or {\"UnusedResources\"," + 594 "\"UnusedIds\"}, or it can be \"all\" to suppress all lint " + 595 "warnings in the given scope.\n" + 596 "\n" + 597 "To suppress a lint warning in an XML file, add a " + 598 "tools:ignore=\"id\" attribute on the element containing " + 599 "the error, or one of its surrounding elements. You also " + 600 "need to define the namespace for the tools prefix on the " + 601 "root element in your document, next to the xmlns:android " + 602 "declaration:\n" + 603 "* xmlns:tools=\"http://schemas.android.com/tools\"\n" + 604 "\n" + 605 "To suppress lint warnings with a configuration XML file, " + 606 "create a file named lint.xml and place it at the root " + 607 "directory of the project in which it applies. (If you " + 608 "use the Eclipse plugin's Lint view, you can suppress " + 609 "errors there via the toolbar and Eclipse will create the " + 610 "lint.xml file for you.).\n" + 611 "\n" + 612 "The format of the lint.xml file is something like the " + 613 "following:\n" + 614 "\n" + 615 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + 616 "<lint>\n" + 617 " <!-- Disable this given check in this project -->\n" + 618 " <issue id=\"IconMissingDensityFolder\" severity=\"ignore\" />\n" + 619 "\n" + 620 " <!-- Ignore the ObsoleteLayoutParam issue in the given files -->\n" + 621 " <issue id=\"ObsoleteLayoutParam\">\n" + 622 " <ignore path=\"res/layout/activation.xml\" />\n" + 623 " <ignore path=\"res/layout-xlarge/activation.xml\" />\n" + 624 " </issue>\n" + 625 "\n" + 626 " <!-- Ignore the UselessLeaf issue in the given file -->\n" + 627 " <issue id=\"UselessLeaf\">\n" + 628 " <ignore path=\"res/layout/main.xml\" />\n" + 629 " </issue>\n" + 630 "\n" + 631 " <!-- Change the severity of hardcoded strings to \"error\" -->\n" + 632 " <issue id=\"HardcodedText\" severity=\"error\" />\n" + 633 "</lint>\n" + 634 "\n" + 635 "To suppress lint checks from the command line, pass the " + ARG_IGNORE + " " + 636 "flag with a comma separated list of ids to be suppressed, such as:\n" + 637 "\"lint --ignore UnusedResources,UselessLeaf /my/project/path\"\n"; 638 } 639 printVersion()640 private void printVersion() { 641 File file = findResource("tools" + File.separator + //$NON-NLS-1$ 642 "source.properties"); //$NON-NLS-1$ 643 if (file.exists()) { 644 FileInputStream input = null; 645 try { 646 input = new FileInputStream(file); 647 Properties properties = new Properties(); 648 properties.load(input); 649 650 String revision = properties.getProperty("Pkg.Revision"); //$NON-NLS-1$ 651 if (revision != null && revision.length() > 0) { 652 System.out.println(String.format("lint: version %1$s", revision)); 653 return; 654 } 655 } catch (IOException e) { 656 // Couldn't find or read the version info: just print out unknown below 657 } finally { 658 Closeables.closeQuietly(input); 659 } 660 } 661 662 System.out.println("lint: unknown version"); 663 } 664 displayValidIds(IssueRegistry registry, PrintStream out)665 private void displayValidIds(IssueRegistry registry, PrintStream out) { 666 List<Category> categories = registry.getCategories(); 667 out.println("Valid issue categories:"); 668 for (Category category : categories) { 669 out.println(" " + category.getFullName()); 670 } 671 out.println(); 672 List<Issue> issues = registry.getIssues(); 673 out.println("Valid issue id's:"); 674 for (Issue issue : issues) { 675 listIssue(out, issue); 676 } 677 } 678 listIssue(PrintStream out, Issue issue)679 private void listIssue(PrintStream out, Issue issue) { 680 out.print(wrapArg("\"" + issue.getId() + "\": " + issue.getDescription())); 681 } 682 showIssues(IssueRegistry registry)683 private void showIssues(IssueRegistry registry) { 684 List<Issue> issues = registry.getIssues(); 685 List<Issue> sorted = new ArrayList<Issue>(issues); 686 Collections.sort(sorted, new Comparator<Issue>() { 687 @Override 688 public int compare(Issue issue1, Issue issue2) { 689 int d = issue1.getCategory().compareTo(issue2.getCategory()); 690 if (d != 0) { 691 return d; 692 } 693 d = issue2.getPriority() - issue1.getPriority(); 694 if (d != 0) { 695 return d; 696 } 697 698 return issue1.getId().compareTo(issue2.getId()); 699 } 700 }); 701 702 System.out.println("Available issues:\n"); 703 Category previousCategory = null; 704 for (Issue issue : sorted) { 705 Category category = issue.getCategory(); 706 if (!category.equals(previousCategory)) { 707 String name = category.getFullName(); 708 System.out.println(name); 709 for (int i = 0, n = name.length(); i < n; i++) { 710 System.out.print('='); 711 } 712 System.out.println('\n'); 713 previousCategory = category; 714 } 715 716 describeIssue(issue); 717 System.out.println(); 718 } 719 } 720 describeIssue(Issue issue)721 private void describeIssue(Issue issue) { 722 System.out.println(issue.getId()); 723 for (int i = 0; i < issue.getId().length(); i++) { 724 System.out.print('-'); 725 } 726 System.out.println(); 727 System.out.println(wrap("Summary: " + issue.getDescription())); 728 System.out.println("Priority: " + issue.getPriority() + " / 10"); 729 System.out.println("Severity: " + issue.getDefaultSeverity().getDescription()); 730 System.out.println("Category: " + issue.getCategory().getFullName()); 731 732 if (!issue.isEnabledByDefault()) { 733 System.out.println("NOTE: This issue is disabled by default!"); 734 System.out.println(String.format("You can enable it by adding %1$s %2$s", ARG_ENABLE, 735 issue.getId())); 736 } 737 738 if (issue.getExplanation() != null) { 739 System.out.println(); 740 System.out.println(wrap(issue.getExplanation())); 741 } 742 if (issue.getMoreInfo() != null) { 743 System.out.println("More information: " + issue.getMoreInfo()); 744 } 745 } 746 wrapArg(String explanation)747 static String wrapArg(String explanation) { 748 // Wrap arguments such that the wrapped lines are not showing up in the left column 749 return wrap(explanation, MAX_LINE_WIDTH, " "); 750 } 751 wrap(String explanation)752 static String wrap(String explanation) { 753 return wrap(explanation, MAX_LINE_WIDTH, ""); 754 } 755 wrap(String explanation, int lineWidth, String hangingIndent)756 static String wrap(String explanation, int lineWidth, String hangingIndent) { 757 int explanationLength = explanation.length(); 758 StringBuilder sb = new StringBuilder(explanationLength * 2); 759 int index = 0; 760 761 while (index < explanationLength) { 762 int lineEnd = explanation.indexOf('\n', index); 763 int next; 764 765 if (lineEnd != -1 && (lineEnd - index) < lineWidth) { 766 next = lineEnd + 1; 767 } else { 768 // Line is longer than available width; grab as much as we can 769 lineEnd = Math.min(index + lineWidth, explanationLength); 770 if (lineEnd - index < lineWidth) { 771 next = explanationLength; 772 } else { 773 // then back up to the last space 774 int lastSpace = explanation.lastIndexOf(' ', lineEnd); 775 if (lastSpace > index) { 776 lineEnd = lastSpace; 777 next = lastSpace + 1; 778 } else { 779 // No space anywhere on the line: it contains something wider than 780 // can fit (like a long URL) so just hard break it 781 next = lineEnd + 1; 782 } 783 } 784 } 785 786 if (sb.length() > 0) { 787 sb.append(hangingIndent); 788 } else { 789 lineWidth -= hangingIndent.length(); 790 } 791 792 sb.append(explanation.substring(index, lineEnd)); 793 sb.append('\n'); 794 index = next; 795 } 796 797 return sb.toString(); 798 } 799 printUsage(PrintStream out)800 private static void printUsage(PrintStream out) { 801 // TODO: Look up launcher script name! 802 String command = "lint"; //$NON-NLS-1$ 803 804 out.println("Usage: " + command + " [flags] <project directories>\n"); 805 out.println("Flags:\n"); 806 807 printUsage(out, new String[] { 808 ARG_HELP, "This message.", 809 ARG_HELP + " <topic>", "Help on the given topic, such as \"suppress\".", 810 ARG_LISTIDS, "List the available issue id's and exit.", 811 ARG_VERSION, "Output version information and exit.", 812 ARG_EXITCODE, "Set the exit code to " + ERRNO_ERRORS + " if errors are found.", 813 ARG_SHOW, "List available issues along with full explanations.", 814 ARG_SHOW + " <ids>", "Show full explanations for the given list of issue id's.", 815 816 "", "\nEnabled Checks:", 817 ARG_DISABLE + " <list>", "Disable the list of categories or " + 818 "specific issue id's. The list should be a comma-separated list of issue " + 819 "id's or categories.", 820 ARG_ENABLE + " <list>", "Enable the specific list of issues. " + 821 "This checks all the default issues plus the specifically enabled issues. The " + 822 "list should be a comma-separated list of issue id's or categories.", 823 ARG_CHECK + " <list>", "Only check the specific list of issues. " + 824 "This will disable everything and re-enable the given list of issues. " + 825 "The list should be a comma-separated list of issue id's or categories.", 826 ARG_NOWARN1 + ", " + ARG_NOWARN2, "Only check for errors (ignore warnings)", 827 ARG_WARNALL, "Check all warnings, including those off by default", 828 ARG_ALLERROR, "Treat all warnings as errors", 829 ARG_CONFIG + " <filename>", "Use the given configuration file to " + 830 "determine whether issues are enabled or disabled. If a project contains " + 831 "a lint.xml file, then this config file will be used as a fallback.", 832 833 834 "", "\nOutput Options:", 835 ARG_QUIET, "Don't show progress.", 836 ARG_FULLPATH, "Use full paths in the error output.", 837 ARG_SHOWALL, "Do not truncate long messages, lists of alternate locations, etc.", 838 ARG_NOLINES, "Do not include the source file lines with errors " + 839 "in the output. By default, the error output includes snippets of source code " + 840 "on the line containing the error, but this flag turns it off.", 841 ARG_HTML + " <filename>", "Create an HTML report instead. If the filename is a " + 842 "directory (or a new filename without an extension), lint will create a " + 843 "separate report for each scanned project.", 844 ARG_URL + " filepath=url", "Add links to HTML report, replacing local " + 845 "path prefixes with url prefix. The mapping can be a comma-separated list of " + 846 "path prefixes to corresponding URL prefixes, such as " + 847 "C:\\temp\\Proj1=http://buildserver/sources/temp/Proj1. To turn off linking " + 848 "to files, use " + ARG_URL + " " + VALUE_NONE, 849 ARG_SIMPLEHTML + " <filename>", "Create a simple HTML report", 850 ARG_XML + " <filename>", "Create an XML report instead.", 851 852 "", "\nExit Status:", 853 "0", "Success.", 854 Integer.toString(ERRNO_ERRORS), "Lint errors detected.", 855 Integer.toString(ERRNO_USAGE), "Lint usage.", 856 Integer.toString(ERRNO_EXISTS), "Cannot clobber existing file.", 857 Integer.toString(ERRNO_HELP), "Lint help.", 858 Integer.toString(ERRNO_INVALIDARGS), "Invalid command-line argument.", 859 }); 860 } 861 printUsage(PrintStream out, String[] args)862 private static void printUsage(PrintStream out, String[] args) { 863 int argWidth = 0; 864 for (int i = 0; i < args.length; i += 2) { 865 String arg = args[i]; 866 argWidth = Math.max(argWidth, arg.length()); 867 } 868 argWidth += 2; 869 StringBuilder sb = new StringBuilder(); 870 for (int i = 0; i < argWidth; i++) { 871 sb.append(' '); 872 } 873 String indent = sb.toString(); 874 String formatString = "%1$-" + argWidth + "s%2$s"; //$NON-NLS-1$ 875 876 for (int i = 0; i < args.length; i += 2) { 877 String arg = args[i]; 878 String description = args[i + 1]; 879 if (arg.length() == 0) { 880 out.println(description); 881 } else { 882 out.print(wrap(String.format(formatString, arg, description), 883 MAX_LINE_WIDTH, indent)); 884 } 885 } 886 } 887 888 @Override log(Severity severity, Throwable exception, String format, Object... args)889 public void log(Severity severity, Throwable exception, String format, Object... args) { 890 System.out.flush(); 891 if (!mQuiet) { 892 // Place the error message on a line of its own since we're printing '.' etc 893 // with newlines during analysis 894 System.err.println(); 895 } 896 if (format != null) { 897 System.err.println(String.format(format, args)); 898 } 899 if (exception != null) { 900 exception.printStackTrace(); 901 } 902 } 903 904 @Override getDomParser()905 public IDomParser getDomParser() { 906 return new LintCliXmlParser(); 907 } 908 909 @Override getConfiguration(Project project)910 public Configuration getConfiguration(Project project) { 911 return new CliConfiguration(mDefaultConfiguration, project); 912 } 913 914 /** File content cache */ 915 private Map<File, String> mFileContents = new HashMap<File, String>(100); 916 917 /** Read the contents of the given file, possibly cached */ getContents(File file)918 private String getContents(File file) { 919 String s = mFileContents.get(file); 920 if (s == null) { 921 s = readFile(file); 922 if (s == null) { 923 s = ""; 924 } 925 mFileContents.put(file, s); 926 } 927 928 return s; 929 } 930 931 @Override getJavaParser()932 public IJavaParser getJavaParser() { 933 return new LombokParser(); 934 } 935 936 @Override report(Context context, Issue issue, Severity severity, Location location, String message, Object data)937 public void report(Context context, Issue issue, Severity severity, Location location, 938 String message, Object data) { 939 assert context.isEnabled(issue); 940 941 if (severity == Severity.IGNORE) { 942 return; 943 } 944 945 if (severity == Severity.FATAL) { 946 // From here on, treat the fatal error as an error such that we don't display 947 // both "Fatal:" and "Error:" etc in the error output. 948 severity = Severity.ERROR; 949 } 950 if (severity == Severity.ERROR) { 951 mHasErrors = true; 952 mErrorCount++; 953 } else { 954 mWarningCount++; 955 } 956 957 Warning warning = new Warning(issue, message, severity, context.getProject(), data); 958 mWarnings.add(warning); 959 960 if (location != null) { 961 warning.location = location; 962 File file = location.getFile(); 963 if (file != null) { 964 warning.file = file; 965 warning.path = getDisplayPath(context.getProject(), file); 966 } 967 968 Position startPosition = location.getStart(); 969 if (startPosition != null) { 970 int line = startPosition.getLine(); 971 warning.line = line; 972 warning.offset = startPosition.getOffset(); 973 if (line >= 0) { 974 if (context.file == location.getFile()) { 975 warning.fileContents = context.getContents(); 976 } 977 if (warning.fileContents == null) { 978 warning.fileContents = getContents(location.getFile()); 979 } 980 981 if (mShowLines) { 982 // Compute error line contents 983 warning.errorLine = getLine(warning.fileContents, line); 984 if (warning.errorLine != null) { 985 // Replace tabs with spaces such that the column 986 // marker (^) lines up properly: 987 warning.errorLine = warning.errorLine.replace('\t', ' '); 988 int column = startPosition.getColumn(); 989 if (column < 0) { 990 column = 0; 991 for (int i = 0; i < warning.errorLine.length(); i++, column++) { 992 if (!Character.isWhitespace(warning.errorLine.charAt(i))) { 993 break; 994 } 995 } 996 } 997 StringBuilder sb = new StringBuilder(); 998 sb.append(warning.errorLine); 999 sb.append('\n'); 1000 for (int i = 0; i < column; i++) { 1001 sb.append(' '); 1002 } 1003 sb.append('^'); 1004 sb.append('\n'); 1005 warning.errorLine = sb.toString(); 1006 } 1007 } 1008 } 1009 } 1010 } 1011 } 1012 1013 /** Look up the contents of the given line */ getLine(String contents, int line)1014 static String getLine(String contents, int line) { 1015 int index = getLineOffset(contents, line); 1016 if (index != -1) { 1017 return getLineOfOffset(contents, index); 1018 } else { 1019 return null; 1020 } 1021 } 1022 getLineOfOffset(String contents, int offset)1023 static String getLineOfOffset(String contents, int offset) { 1024 int end = contents.indexOf('\n', offset); 1025 return contents.substring(offset, end != -1 ? end : contents.length()); 1026 } 1027 1028 1029 /** Look up the contents of the given line */ getLineOffset(String contents, int line)1030 static int getLineOffset(String contents, int line) { 1031 int index = 0; 1032 for (int i = 0; i < line; i++) { 1033 index = contents.indexOf('\n', index); 1034 if (index == -1) { 1035 return -1; 1036 } 1037 index++; 1038 } 1039 1040 return index; 1041 } 1042 1043 @Override readFile(File file)1044 public String readFile(File file) { 1045 try { 1046 return LintUtils.getEncodedString(file); 1047 } catch (IOException e) { 1048 return ""; //$NON-NLS-1$ 1049 } 1050 } 1051 isCheckingSpecificIssues()1052 boolean isCheckingSpecificIssues() { 1053 return mCheck != null; 1054 } 1055 1056 /** 1057 * Consult the lint.xml file, but override with the --enable and --disable 1058 * flags supplied on the command line 1059 */ 1060 class CliConfiguration extends DefaultConfiguration { CliConfiguration(Configuration parent, Project project)1061 CliConfiguration(Configuration parent, Project project) { 1062 super(Main.this, project, parent); 1063 } 1064 CliConfiguration(File lintFile)1065 CliConfiguration(File lintFile) { 1066 super(Main.this, null /*project*/, null /*parent*/, lintFile); 1067 } 1068 1069 @Override getSeverity(Issue issue)1070 public Severity getSeverity(Issue issue) { 1071 Severity severity = computeSeverity(issue); 1072 1073 if (mAllErrors && severity != Severity.IGNORE) { 1074 severity = Severity.ERROR; 1075 } 1076 1077 if (mNoWarnings && severity == Severity.WARNING) { 1078 severity = Severity.IGNORE; 1079 } 1080 1081 return severity; 1082 } 1083 1084 @Override getDefaultSeverity(Issue issue)1085 protected Severity getDefaultSeverity(Issue issue) { 1086 if (mWarnAll) { 1087 return issue.getDefaultSeverity(); 1088 } 1089 1090 return super.getDefaultSeverity(issue); 1091 } 1092 computeSeverity(Issue issue)1093 private Severity computeSeverity(Issue issue) { 1094 Severity severity = super.getSeverity(issue); 1095 1096 String id = issue.getId(); 1097 if (mSuppress.contains(id)) { 1098 return Severity.IGNORE; 1099 } 1100 1101 if (mEnabled.contains(id) || (mCheck != null && mCheck.contains(id))) { 1102 // Overriding default 1103 // Detectors shouldn't be returning ignore as a default severity, 1104 // but in case they do, force it up to warning here to ensure that 1105 // it's run 1106 if (severity == Severity.IGNORE) { 1107 severity = issue.getDefaultSeverity(); 1108 if (severity == Severity.IGNORE) { 1109 severity = Severity.WARNING; 1110 } 1111 } 1112 1113 return severity; 1114 } 1115 1116 if (mCheck != null && issue != LINT_ERROR && issue != PARSER_ERROR) { 1117 return Severity.IGNORE; 1118 } 1119 1120 return severity; 1121 } 1122 } 1123 1124 private class ProgressPrinter implements LintListener { 1125 @Override update(LintDriver lint, EventType type, Context context)1126 public void update(LintDriver lint, EventType type, Context context) { 1127 switch (type) { 1128 case SCANNING_PROJECT: 1129 if (lint.getPhase() > 1) { 1130 System.out.print(String.format( 1131 "\nScanning %1$s (Phase %2$d): ", 1132 context.getProject().getName(), 1133 lint.getPhase())); 1134 } else { 1135 System.out.print(String.format( 1136 "\nScanning %1$s: ", 1137 context.getProject().getName())); 1138 } 1139 break; 1140 case SCANNING_LIBRARY_PROJECT: 1141 System.out.print(String.format( 1142 "\n - %1$s: ", 1143 context.getProject().getName())); 1144 break; 1145 case SCANNING_FILE: 1146 System.out.print('.'); 1147 break; 1148 case NEW_PHASE: 1149 // Ignored for now: printing status as part of next project's status 1150 break; 1151 case CANCELED: 1152 case COMPLETED: 1153 System.out.println(); 1154 break; 1155 } 1156 } 1157 } 1158 getDisplayPath(Project project, File file)1159 String getDisplayPath(Project project, File file) { 1160 String path = file.getPath(); 1161 if (!mFullPath && path.startsWith(project.getReferenceDir().getPath())) { 1162 int chop = project.getReferenceDir().getPath().length(); 1163 if (path.length() > chop && path.charAt(chop) == File.separatorChar) { 1164 chop++; 1165 } 1166 path = path.substring(chop); 1167 if (path.length() == 0) { 1168 path = file.getName(); 1169 } 1170 } 1171 1172 return path; 1173 } 1174 1175 /** Returns whether all warnings are enabled, including those disabled by default */ isAllEnabled()1176 boolean isAllEnabled() { 1177 return mWarnAll; 1178 } 1179 1180 /** Returns the issue registry used by this client */ getRegistry()1181 IssueRegistry getRegistry() { 1182 return mRegistry; 1183 } 1184 1185 /** Returns the driver running the lint checks */ getDriver()1186 LintDriver getDriver() { 1187 return mDriver; 1188 } 1189 1190 /** Returns the configuration used by this client */ getConfiguration()1191 Configuration getConfiguration() { 1192 return mDefaultConfiguration; 1193 } 1194 1195 /** Returns true if the given issue has been explicitly disabled */ isSuppressed(Issue issue)1196 boolean isSuppressed(Issue issue) { 1197 return mSuppress.contains(issue.getId()); 1198 } 1199 } 1200