• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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