fixing serialization for android gradle model
[idea/adt-tools-base.git] / lint / cli / src / main / java / com / android / tools / lint / Main.java
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.SdkConstants.DOT_XML;
20 import static com.android.SdkConstants.VALUE_NONE;
21 import static com.android.tools.lint.LintCliFlags.ERRNO_ERRORS;
22 import static com.android.tools.lint.LintCliFlags.ERRNO_EXISTS;
23 import static com.android.tools.lint.LintCliFlags.ERRNO_HELP;
24 import static com.android.tools.lint.LintCliFlags.ERRNO_INVALID_ARGS;
25 import static com.android.tools.lint.LintCliFlags.ERRNO_SUCCESS;
26 import static com.android.tools.lint.LintCliFlags.ERRNO_USAGE;
27 import static com.android.tools.lint.detector.api.Lint.endsWith;
28 import static com.android.tools.lint.detector.api.TextFormat.TEXT;
29
30 import com.android.SdkConstants;
31 import com.android.annotations.NonNull;
32 import com.android.annotations.Nullable;
33 import com.android.tools.lint.checks.BuiltinIssueRegistry;
34 import com.android.tools.lint.client.api.Configuration;
35 import com.android.tools.lint.client.api.IssueRegistry;
36 import com.android.tools.lint.client.api.LintClient;
37 import com.android.tools.lint.client.api.LintDriver;
38 import com.android.tools.lint.client.api.LintRequest;
39 import com.android.tools.lint.detector.api.Category;
40 import com.android.tools.lint.detector.api.Context;
41 import com.android.tools.lint.detector.api.Issue;
42 import com.android.tools.lint.detector.api.Lint;
43 import com.android.tools.lint.detector.api.Location;
44 import com.android.tools.lint.detector.api.Project;
45 import com.android.tools.lint.detector.api.Scope;
46 import com.android.tools.lint.detector.api.Severity;
47 import com.android.tools.lint.detector.api.TextFormat;
48 import com.android.utils.SdkUtils;
49 import com.android.utils.XmlUtils;
50 import com.google.common.annotations.Beta;
51 import com.google.common.annotations.VisibleForTesting;
52 import com.google.common.base.Charsets;
53 import com.google.common.io.ByteStreams;
54 import java.io.BufferedWriter;
55 import java.io.File;
56 import java.io.FileWriter;
57 import java.io.IOException;
58 import java.io.InputStream;
59 import java.io.PrintStream;
60 import java.io.PrintWriter;
61 import java.io.Writer;
62 import java.util.ArrayList;
63 import java.util.Collection;
64 import java.util.EnumSet;
65 import java.util.HashMap;
66 import java.util.HashSet;
67 import java.util.List;
68 import java.util.Locale;
69 import java.util.Map;
70 import java.util.Set;
71 import java.util.regex.Pattern;
72 import java.util.zip.ZipEntry;
73 import java.util.zip.ZipException;
74 import java.util.zip.ZipFile;
75 import org.w3c.dom.Document;
76 import org.xml.sax.SAXException;
77
78 /**
79  * Command line driver for the lint framework
80  *
81  * <p><b>NOTE: This is not a public or final API; if you rely on this be prepared to adjust your
82  * code for the next tools release.</b>
83  */
84 @Beta
85 public class Main {
86     static final int MAX_LINE_WIDTH = 78;
87     private static final String ARG_ENABLE = "--enable";
88     private static final String ARG_DISABLE = "--disable";
89     private static final String ARG_CHECK = "--check";
90     private static final String ARG_AUTO_FIX = "--apply-suggestions";
91     private static final String ARG_DESCRIBE_FIXES = "--describe-suggestions";
92     private static final String ARG_IGNORE = "--ignore";
93     private static final String ARG_LIST_IDS = "--list";
94     private static final String ARG_SHOW = "--show";
95     private static final String ARG_QUIET = "--quiet";
96     private static final String ARG_FULL_PATH = "--fullpath";
97     private static final String ARG_SHOW_ALL = "--showall";
98     private static final String ARG_HELP = "--help";
99     private static final String ARG_NO_LINES = "--nolines";
100     private static final String ARG_HTML = "--html";
101     private static final String ARG_SIMPLE_HTML = "--simplehtml";
102     private static final String ARG_XML = "--xml";
103     private static final String ARG_TEXT = "--text";
104     private static final String ARG_CONFIG = "--config";
105     private static final String ARG_URL = "--url";
106     private static final String ARG_VERSION = "--version";
107     private static final String ARG_EXIT_CODE = "--exitcode";
108     private static final String ARG_SDK_HOME = "--sdk-home";
109     private static final String ARG_FATAL = "--fatalOnly";
110     private static final String ARG_PROJECT = "--project";
111     private static final String ARG_CLASSES = "--classpath";
112     private static final String ARG_SOURCES = "--sources";
113     private static final String ARG_RESOURCES = "--resources";
114     private static final String ARG_LIBRARIES = "--libraries";
115     private static final String ARG_BUILD_API = "--compile-sdk-version";
116     private static final String ARG_BASELINE = "--baseline";
117     private static final String ARG_REMOVE_FIXED = "--remove-fixed";
118     private static final String ARG_UPDATE_BASELINE = "--update-baseline";
119     private static final String ARG_ALLOW_SUPPRESS = "--allow-suppress";
120     private static final String ARG_RESTRICT_SUPPRESS = "--restrict-suppress";
121
122     private static final String ARG_NO_WARN_2 = "--nowarn";
123     // GCC style flag names for options
124     private static final String ARG_NO_WARN_1 = "-w";
125     private static final String ARG_WARN_ALL = "-Wall";
126     private static final String ARG_ALL_ERROR = "-Werror";
127
128     private static final String PROP_WORK_DIR = "com.android.tools.lint.workdir";
129     private final LintCliFlags flags = new LintCliFlags();
130     private IssueRegistry globalIssueRegistry;
131     @Nullable private File sdkHome;
132
133     /** Creates a CLI driver */
134     public Main() {}
135
136     /**
137      * Runs the static analysis command line driver
138      *
139      * @param args program arguments
140      */
141     public static void main(String[] args) {
142         try {
143             new Main().run(args);
144         } catch (ExitException exitException) {
145             System.exit(exitException.getStatus());
146         }
147     }
148
149     /** Hook intended for tests */
150     protected void initializeDriver(@NonNull LintDriver driver) {}
151
152     /**
153      * Runs the static analysis command line driver
154      *
155      * @param args program arguments
156      */
157     @SuppressWarnings("UnnecessaryLocalVariable")
158     public void run(String[] args) {
159         if (args.length < 1) {
160             printUsage(System.err);
161             exit(ERRNO_USAGE);
162         }
163
164         // When running lint from the command line, warn if the project is a Gradle project
165         // since those projects may have custom project configuration that the command line
166         // runner won't know about.
167         LintCliClient client =
168                 new LintCliClient(flags, LintClient.CLIENT_CLI) {
169
170                     private Pattern mAndroidAnnotationPattern;
171                     private Project unexpectedGradleProject = null;
172
173                     @Override
174                     @NonNull
175                     protected LintDriver createDriver(
176                             @NonNull IssueRegistry registry, @NonNull LintRequest request) {
177                         LintDriver driver = super.createDriver(registry, request);
178
179                         Project project = unexpectedGradleProject;
180                         if (project != null) {
181                             String message =
182                                     String.format(
183                                             "\"`%1$s`\" is a Gradle project. To correctly "
184                                                     + "analyze Gradle projects, you should run \"`gradlew :lint`\" "
185                                                     + "instead.",
186                                             project.getName());
187                             Location location =
188                                     Lint.guessGradleLocation(this, project.getDir(), null);
189                             LintClient.Companion.report(
190                                     this,
191                                     IssueRegistry.LINT_ERROR,
192                                     message,
193                                     driver,
194                                     project,
195                                     location,
196                                     null);
197                         }
198
199                         initializeDriver(driver);
200
201                         return driver;
202                     }
203
204                     @NonNull
205                     @Override
206                     protected Project createProject(@NonNull File dir, @NonNull File referenceDir) {
207                         Project project = super.createProject(dir, referenceDir);
208                         if (project.isGradleProject()) {
209                             // Can't report error yet; stash it here so we can report it after the
210                             // driver has been created
211                             unexpectedGradleProject = project;
212                         }
213
214                         return project;
215                     }
216
217                     @NonNull
218                     @Override
219                     public Configuration getConfiguration(
220                             @NonNull final Project project, @Nullable LintDriver driver) {
221                         if (overrideConfiguration != null) {
222                             return overrideConfiguration;
223                         }
224
225                         if (project.isGradleProject()) {
226                             // Don't report any issues when analyzing a Gradle project from the
227                             // non-Gradle runner; they are likely to be false, and will hide the real
228                             // problem reported above
229                             //noinspection ReturnOfInnerClass
230                             return new CliConfiguration(getConfiguration(), project, true) {
231                                 @NonNull
232                                 @Override
233                                 public Severity getSeverity(@NonNull Issue issue) {
234                                     return issue == IssueRegistry.LINT_ERROR
235                                             ? Severity.FATAL
236                                             : Severity.IGNORE;
237                                 }
238
239                                 @Override
240                                 public boolean isIgnored(
241                                         @NonNull Context context,
242                                         @NonNull Issue issue,
243                                         @Nullable Location location,
244                                         @NonNull String message) {
245                                     // If you've deliberately ignored IssueRegistry.LINT_ERROR
246                                     // don't flag that one either
247                                     if (issue == IssueRegistry.LINT_ERROR
248                                             && new LintCliClient(flags, LintClient.getClientName())
249                                                     .isSuppressed(IssueRegistry.LINT_ERROR)) {
250                                         return true;
251                                     }
252
253                                     return issue != IssueRegistry.LINT_ERROR;
254                                 }
255                             };
256                         }
257                         return super.getConfiguration(project, driver);
258                     }
259
260                     private byte[] readSrcJar(@NonNull File file) {
261                         String path = file.getPath();
262                         int srcJarIndex = path.indexOf("srcjar!");
263                         if (srcJarIndex != -1) {
264                             File jarFile = new File(path.substring(0, srcJarIndex + 6));
265                             if (jarFile.exists()) {
266                                 try (ZipFile zipFile = new ZipFile(jarFile)) {
267                                     String name =
268                                             path.substring(srcJarIndex + 8)
269                                                     .replace(File.separatorChar, '/');
270                                     ZipEntry entry = zipFile.getEntry(name);
271                                     if (entry != null) {
272                                         try (InputStream is = zipFile.getInputStream(entry)) {
273                                             byte[] bytes = ByteStreams.toByteArray(is);
274                                             return bytes;
275                                         } catch (Exception e) {
276                                             log(e, null);
277                                         }
278                                     }
279                                 } catch (ZipException e) {
280                                     Main.this.log(e, "Could not unzip %1$s", jarFile);
281                                 } catch (IOException e) {
282                                     Main.this.log(e, "Could not read %1$s", jarFile);
283                                 }
284                             }
285                         }
286
287                         return null;
288                     }
289
290                     @NonNull
291                     @Override
292                     public CharSequence readFile(@NonNull File file) {
293                         // .srcjar file handle?
294                         byte[] srcJarBytes = readSrcJar(file);
295                         if (srcJarBytes != null) {
296                             return new String(srcJarBytes, Charsets.UTF_8);
297                         }
298
299                         CharSequence contents = super.readFile(file);
300                         if (Project.isAospBuildEnvironment()
301                                 && file.getPath().endsWith(SdkConstants.DOT_JAVA)) {
302                             if (mAndroidAnnotationPattern == null) {
303                                 mAndroidAnnotationPattern = Pattern.compile("android\\.annotation");
304                             }
305                             return mAndroidAnnotationPattern
306                                     .matcher(contents)
307                                     .replaceAll("android.support.annotation");
308                         } else {
309                             return contents;
310                         }
311                     }
312
313                     @NonNull
314                     @Override
315                     public byte[] readBytes(@NonNull File file) throws IOException {
316                         // .srcjar file handle?
317                         byte[] srcJarBytes = readSrcJar(file);
318                         if (srcJarBytes != null) {
319                             return srcJarBytes;
320                         }
321
322                         return super.readBytes(file);
323                     }
324
325                     private ProjectMetadata metadata;
326
327                     /** Creates a lint request */
328                     @Override
329                     @NonNull
330                     protected LintRequest createLintRequest(@NonNull List<File> files) {
331                         LintRequest request = super.createLintRequest(files);
332                         File descriptor = flags.getProjectDescriptorOverride();
333                         if (descriptor != null) {
334                             metadata = ProjectInitializerKt.computeMetadata(this, descriptor);
335                             List<Project> projects = metadata.getProjects();
336                             if (!projects.isEmpty()) {
337                                 request.setProjects(projects);
338
339                                 if (metadata.getSdk() != null) {
340                                     sdkHome = metadata.getSdk();
341                                 }
342
343                                 if (metadata.getBaseline() != null) {
344                                     flags.setBaselineFile(metadata.getBaseline());
345                                 }
346
347                                 EnumSet<Scope> scope = EnumSet.copyOf(Scope.ALL);
348                                 if (metadata.getIncomplete()) {
349                                     scope.remove(Scope.ALL_CLASS_FILES);
350                                     scope.remove(Scope.ALL_JAVA_FILES);
351                                     scope.remove(Scope.ALL_RESOURCE_FILES);
352                                 }
353                                 request.setScope(scope);
354
355                                 request.setPlatform(metadata.getPlatforms());
356                             }
357                         }
358
359                         return request;
360                     }
361
362                     @NonNull
363                     @Override
364                     public List<File> findRuleJars(@NonNull Project project) {
365                         if (metadata != null) {
366                             List<File> jars = metadata.getLintChecks().get(project);
367                             if (jars != null) {
368                                 return jars;
369                             }
370                         }
371
372                         return super.findRuleJars(project);
373                     }
374
375                     @NonNull
376                     @Override
377                     public List<File> findGlobalRuleJars() {
378                         if (metadata != null) {
379                             List<File> jars = metadata.getGlobalLintChecks();
380                             if (!jars.isEmpty()) {
381                                 return jars;
382                             }
383                         }
384
385                         return super.findGlobalRuleJars();
386                     }
387
388                     @Nullable
389                     @Override
390                     public File getCacheDir(@Nullable String name, boolean create) {
391                         if (metadata != null) {
392                             File dir = metadata.getCache();
393                             if (dir != null) {
394                                 if (name != null) {
395                                     dir = new File(dir, name);
396                                 }
397
398                                 if (create && !dir.exists()) {
399                                     if (!dir.mkdirs()) {
400                                         return null;
401                                     }
402                                 }
403                                 return dir;
404                             }
405                         }
406
407                         return super.getCacheDir(name, create);
408                     }
409
410                     @Nullable
411                     @Override
412                     public Document getMergedManifest(@NonNull Project project) {
413                         if (metadata != null) {
414                             File manifest = metadata.getMergedManifests().get(project);
415                             if (manifest != null && manifest.exists()) {
416                                 try {
417                                     // We can't call
418                                     //   resolveMergeManifestSources(document, manifestReportFile)
419                                     // here since we don't have the merging log.
420                                     return XmlUtils.parseUtfXmlFile(manifest, true);
421                                 } catch (IOException | SAXException e) {
422                                     log(e, "Could not read/parse %1$s", manifest);
423                                 }
424                             }
425                         }
426
427                         return super.getMergedManifest(project);
428                     }
429
430                     @Nullable
431                     @Override
432                     public File getSdkHome() {
433                         if (Main.this.sdkHome != null) {
434                             return Main.this.sdkHome;
435                         }
436                         return super.getSdkHome();
437                     }
438
439                     @Override
440                     protected boolean addBootClassPath(
441                             @NonNull Collection<? extends Project> knownProjects,
442                             List<File> files) {
443                         if (metadata != null && !metadata.getJdkBootClasspath().isEmpty()) {
444                             boolean isAndroid = false;
445                             for (Project project : knownProjects) {
446                                 if (project.isAndroidProject()) {
447                                     isAndroid = true;
448                                     break;
449                                 }
450                             }
451                             if (!isAndroid) {
452                                 files.addAll(metadata.getJdkBootClasspath());
453                                 return true;
454                             }
455
456                             boolean ok = super.addBootClassPath(knownProjects, files);
457                             if (!ok) {
458                                 files.addAll(metadata.getJdkBootClasspath());
459                             }
460                             return ok;
461                         }
462
463                         return super.addBootClassPath(knownProjects, files);
464                     }
465
466                     @NonNull
467                     @Override
468                     public List<File> getExternalAnnotations(
469                             @NonNull Collection<? extends Project> projects) {
470                         List<File> externalAnnotations = super.getExternalAnnotations(projects);
471                         if (metadata != null) {
472                             externalAnnotations.addAll(metadata.getExternalAnnotations());
473                         }
474                         return externalAnnotations;
475                     }
476                 };
477
478         // Mapping from file path prefix to URL. Applies only to HTML reports
479         String urlMap = null;
480
481         List<File> files = new ArrayList<>();
482         for (int index = 0; index < args.length; index++) {
483             String arg = args[index];
484
485             if (arg.equals(ARG_HELP) || arg.equals("-h") || arg.equals("-?")) {
486                 if (index < args.length - 1) {
487                     String topic = args[index + 1];
488                     if (topic.equals("suppress") || topic.equals("ignore")) {
489                         printHelpTopicSuppress();
490                         exit(ERRNO_HELP);
491                     } else {
492                         System.err.println(String.format("Unknown help topic \"%1$s\"", topic));
493                         exit(ERRNO_INVALID_ARGS);
494                     }
495                 }
496                 printUsage(System.out);
497                 exit(ERRNO_HELP);
498             } else if (arg.equals(ARG_LIST_IDS)) {
499                 IssueRegistry registry = getGlobalRegistry(client);
500                 // Did the user provide a category list?
501                 if (index < args.length - 1 && !args[index + 1].startsWith("-")) {
502                     String[] ids = args[++index].split(",");
503                     for (String id : ids) {
504                         if (registry.isCategoryName(id)) {
505                             // List all issues with the given category
506                             String category = id;
507                             for (Issue issue : registry.getIssues()) {
508                                 // Check prefix such that filtering on the "Usability" category
509                                 // will match issue category "Usability:Icons" etc.
510                                 if (issue.getCategory().getName().startsWith(category)
511                                         || issue.getCategory().getFullName().startsWith(category)) {
512                                     listIssue(System.out, issue);
513                                 }
514                             }
515                         } else {
516                             System.err.println("Invalid category \"" + id + "\".\n");
517                             displayValidIds(registry, System.err);
518                             exit(ERRNO_INVALID_ARGS);
519                         }
520                     }
521                 } else {
522                     displayValidIds(registry, System.out);
523                 }
524                 exit(ERRNO_SUCCESS);
525             } else if (arg.equals(ARG_SHOW)) {
526                 IssueRegistry registry = getGlobalRegistry(client);
527                 // Show specific issues?
528                 if (index < args.length - 1 && !args[index + 1].startsWith("-")) {
529                     String[] ids = args[++index].split(",");
530                     for (String id : ids) {
531                         if (registry.isCategoryName(id)) {
532                             // Show all issues in the given category
533                             String category = id;
534                             for (Issue issue : registry.getIssues()) {
535                                 // Check prefix such that filtering on the "Usability" category
536                                 // will match issue category "Usability:Icons" etc.
537                                 if (issue.getCategory().getName().startsWith(category)
538                                         || issue.getCategory().getFullName().startsWith(category)) {
539                                     describeIssue(issue);
540                                     System.out.println();
541                                 }
542                             }
543                         } else if (registry.isIssueId(id)) {
544                             describeIssue(registry.getIssue(id));
545                             System.out.println();
546                         } else {
547                             System.err.println("Invalid id or category \"" + id + "\".\n");
548                             displayValidIds(registry, System.err);
549                             exit(ERRNO_INVALID_ARGS);
550                         }
551                     }
552                 } else {
553                     showIssues(registry);
554                 }
555                 exit(ERRNO_SUCCESS);
556             } else if (arg.equals(ARG_FULL_PATH)
557                     || arg.equals(ARG_FULL_PATH + "s")) { // allow "--fullpaths" too
558                 flags.setFullPath(true);
559             } else if (arg.equals(ARG_SHOW_ALL)) {
560                 flags.setShowEverything(true);
561             } else if (arg.equals(ARG_QUIET) || arg.equals("-q")) {
562                 flags.setQuiet(true);
563             } else if (arg.equals(ARG_NO_LINES)) {
564                 flags.setShowSourceLines(false);
565             } else if (arg.equals(ARG_EXIT_CODE)) {
566                 flags.setSetExitCode(true);
567             } else if (arg.equals(ARG_FATAL)) {
568                 flags.setFatalOnly(true);
569             } else if (arg.equals(ARG_VERSION)) {
570                 printVersion(client);
571                 exit(ERRNO_SUCCESS);
572             } else if (arg.equals(ARG_URL)) {
573                 if (index == args.length - 1) {
574                     System.err.println("Missing URL mapping string");
575                     exit(ERRNO_INVALID_ARGS);
576                 }
577                 String map = args[++index];
578                 // Allow repeated usage of the argument instead of just comma list
579                 if (urlMap != null) {
580                     //noinspection StringConcatenationInLoop
581                     urlMap = urlMap + ',' + map;
582                 } else {
583                     urlMap = map;
584                 }
585             } else if (arg.equals(ARG_CONFIG)) {
586                 if (index == args.length - 1 || !endsWith(args[index + 1], DOT_XML)) {
587                     System.err.println("Missing XML configuration file argument");
588                     exit(ERRNO_INVALID_ARGS);
589                 }
590                 File file = getInArgumentPath(args[++index]);
591                 if (!file.exists()) {
592                     System.err.println(file.getAbsolutePath() + " does not exist");
593                     exit(ERRNO_INVALID_ARGS);
594                 }
595                 flags.setDefaultConfiguration(file);
596             } else if (arg.equals(ARG_HTML) || arg.equals(ARG_SIMPLE_HTML)) {
597                 if (index == args.length - 1) {
598                     System.err.println("Missing HTML output file name");
599                     exit(ERRNO_INVALID_ARGS);
600                 }
601                 File output = getOutArgumentPath(args[++index]);
602                 // Get an absolute path such that we can ask its parent directory for
603                 // write permission etc.
604                 output = output.getAbsoluteFile();
605                 if (output.isDirectory()
606                         || (!output.exists() && output.getName().indexOf('.') == -1)) {
607                     if (!output.exists()) {
608                         boolean mkdirs = output.mkdirs();
609                         if (!mkdirs) {
610                             log(null, "Could not create output directory %1$s", output);
611                             exit(ERRNO_EXISTS);
612                         }
613                     }
614                     MultiProjectHtmlReporter reporter =
615                             new MultiProjectHtmlReporter(client, output, flags);
616                     if (arg.equals(ARG_SIMPLE_HTML)) {
617                         System.err.println(ARG_SIMPLE_HTML + " ignored: no longer supported");
618                     }
619                     flags.getReporters().add(reporter);
620                     continue;
621                 }
622                 if (output.exists()) {
623                     boolean delete = output.delete();
624                     if (!delete) {
625                         System.err.println("Could not delete old " + output);
626                         exit(ERRNO_EXISTS);
627                     }
628                 }
629                 if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
630                     System.err.println("Cannot write HTML output file " + output);
631                     exit(ERRNO_EXISTS);
632                 }
633                 try {
634                     Reporter reporter = Reporter.createHtmlReporter(client, output, flags);
635                     flags.getReporters().add(reporter);
636                 } catch (IOException e) {
637                     log(e, null);
638                     exit(ERRNO_INVALID_ARGS);
639                 }
640             } else if (arg.equals(ARG_XML)) {
641                 if (index == args.length - 1) {
642                     System.err.println("Missing XML output file name");
643                     exit(ERRNO_INVALID_ARGS);
644                 }
645                 File output = getOutArgumentPath(args[++index]);
646                 // Get an absolute path such that we can ask its parent directory for
647                 // write permission etc.
648                 output = output.getAbsoluteFile();
649
650                 if (output.exists()) {
651                     boolean delete = output.delete();
652                     if (!delete) {
653                         System.err.println("Could not delete old " + output);
654                         exit(ERRNO_EXISTS);
655                     }
656                 }
657                 if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
658                     System.err.println("Cannot write XML output file " + output);
659                     exit(ERRNO_EXISTS);
660                 }
661                 try {
662                     flags.getReporters()
663                             .add(
664                                     Reporter.createXmlReporter(
665                                             client, output, false, flags.isIncludeXmlFixes()));
666                 } catch (IOException e) {
667                     log(e, null);
668                     exit(ERRNO_INVALID_ARGS);
669                 }
670             } else if (arg.equals(ARG_TEXT)) {
671                 if (index == args.length - 1) {
672                     System.err.println("Missing text output file name");
673                     exit(ERRNO_INVALID_ARGS);
674                 }
675
676                 Writer writer = null;
677                 boolean closeWriter;
678                 String outputName = args[++index];
679                 if (outputName.equals("stdout")) {
680                     //noinspection IOResourceOpenedButNotSafelyClosed,resource
681                     writer = new PrintWriter(System.out, true);
682                     closeWriter = false;
683                 } else {
684                     File output = getOutArgumentPath(outputName);
685
686                     // Get an absolute path such that we can ask its parent directory for
687                     // write permission etc.
688                     output = output.getAbsoluteFile();
689
690                     if (output.exists()) {
691                         boolean delete = output.delete();
692                         if (!delete) {
693                             System.err.println("Could not delete old " + output);
694                             exit(ERRNO_EXISTS);
695                         }
696                     }
697                     if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
698                         System.err.println("Cannot write text output file " + output);
699                         exit(ERRNO_EXISTS);
700                     }
701                     try {
702                         //noinspection IOResourceOpenedButNotSafelyClosed,resource
703                         writer = new BufferedWriter(new FileWriter(output));
704                     } catch (IOException e) {
705                         log(e, null);
706                         exit(ERRNO_INVALID_ARGS);
707                     }
708                     closeWriter = true;
709                 }
710                 flags.getReporters().add(new TextReporter(client, flags, writer, closeWriter));
711             } else if (arg.equals(ARG_DISABLE) || arg.equals(ARG_IGNORE)) {
712                 if (index == args.length - 1) {
713                     System.err.println("Missing categories or id's to disable");
714                     exit(ERRNO_INVALID_ARGS);
715                 }
716                 IssueRegistry registry = getGlobalRegistry(client);
717                 String[] ids = args[++index].split(",");
718                 for (String id : ids) {
719                     if (registry.isCategoryName(id)) {
720                         // Suppress all issues with the given category
721                         String category = id;
722                         for (Issue issue : registry.getIssues()) {
723                             // Check prefix such that filtering on the "Usability" category
724                             // will match issue category "Usability:Icons" etc.
725                             if (issue.getCategory().getName().startsWith(category)
726                                     || issue.getCategory().getFullName().startsWith(category)) {
727                                 flags.getSuppressedIds().add(issue.getId());
728                             }
729                         }
730                     } else {
731                         flags.getSuppressedIds().add(id);
732                     }
733                 }
734             } else if (arg.equals(ARG_ENABLE)) {
735                 if (index == args.length - 1) {
736                     System.err.println("Missing categories or id's to enable");
737                     exit(ERRNO_INVALID_ARGS);
738                 }
739                 IssueRegistry registry = getGlobalRegistry(client);
740                 String[] ids = args[++index].split(",");
741                 for (String id : ids) {
742                     if (registry.isCategoryName(id)) {
743                         // Enable all issues with the given category
744                         String category = id;
745                         for (Issue issue : registry.getIssues()) {
746                             if (issue.getCategory().getName().startsWith(category)
747                                     || issue.getCategory().getFullName().startsWith(category)) {
748                                 flags.getEnabledIds().add(issue.getId());
749                             }
750                         }
751                         flags.getEnabledIds().add(id);
752                     }
753                 }
754             } else if (arg.equals(ARG_CHECK)) {
755                 if (index == args.length - 1) {
756                     System.err.println("Missing categories or id's to check");
757                     exit(ERRNO_INVALID_ARGS);
758                 }
759                 Set<String> checkedIds = flags.getExactCheckedIds();
760                 if (checkedIds == null) {
761                     checkedIds = new HashSet<>();
762                     flags.setExactCheckedIds(checkedIds);
763                 }
764                 IssueRegistry registry = getGlobalRegistry(client);
765                 String[] ids = args[++index].split(",");
766                 for (String id : ids) {
767                     if (registry.isCategoryName(id)) {
768                         // Check all issues with the given category
769                         String category = id;
770                         for (Issue issue : registry.getIssues()) {
771                             // Check prefix such that filtering on the "Usability" category
772                             // will match issue category "Usability:Icons" etc.
773                             if (issue.getCategory().getName().startsWith(category)
774                                     || issue.getCategory().getFullName().startsWith(category)) {
775                                 checkedIds.add(issue.getId());
776                             }
777                         }
778                     } else {
779                         checkedIds.add(id);
780                     }
781                 }
782             } else if (arg.equals(ARG_NO_WARN_1) || arg.equals(ARG_NO_WARN_2)) {
783                 flags.setIgnoreWarnings(true);
784             } else if (arg.equals(ARG_WARN_ALL)) {
785                 flags.setCheckAllWarnings(true);
786             } else if (arg.equals(ARG_ALL_ERROR)) {
787                 flags.setWarningsAsErrors(true);
788             } else if (arg.equals(ARG_AUTO_FIX)) {
789                 flags.setAutoFix(true);
790             } else if (arg.equals(ARG_DESCRIBE_FIXES)) {
791                 flags.setIncludeXmlFixes(true);
792                 // Make sure we also update any XML reporters we've *already* created before
793                 // coming across this flag:
794                 for (Reporter reporter : flags.getReporters()) {
795                     if (reporter instanceof XmlReporter) {
796                         XmlReporter xmlReporter = (XmlReporter) reporter;
797                         if (!xmlReporter.isIntendedForBaseline()) {
798                             xmlReporter.setIncludeFixes(true);
799                         }
800                     }
801                 }
802             } else if (arg.equals(ARG_CLASSES)) {
803                 if (index == args.length - 1) {
804                     System.err.println("Missing class folder name");
805                     exit(ERRNO_INVALID_ARGS);
806                 }
807                 String paths = args[++index];
808                 for (String path : Lint.splitPath(paths)) {
809                     File input = getInArgumentPath(path);
810                     if (!input.exists()) {
811                         System.err.println("Class path entry " + input + " does not exist.");
812                         exit(ERRNO_INVALID_ARGS);
813                     }
814                     List<File> classes = flags.getClassesOverride();
815                     if (classes == null) {
816                         classes = new ArrayList<>();
817                         flags.setClassesOverride(classes);
818                     }
819                     classes.add(input);
820                 }
821             } else if (arg.equals(ARG_SOURCES)) {
822                 if (index == args.length - 1) {
823                     System.err.println("Missing source folder name");
824                     exit(ERRNO_INVALID_ARGS);
825                 }
826                 String paths = args[++index];
827                 for (String path : Lint.splitPath(paths)) {
828                     File input = getInArgumentPath(path);
829                     if (!input.exists()) {
830                         System.err.println("Source folder " + input + " does not exist.");
831                         exit(ERRNO_INVALID_ARGS);
832                     }
833                     List<File> sources = flags.getSourcesOverride();
834                     if (sources == null) {
835                         sources = new ArrayList<>();
836                         flags.setSourcesOverride(sources);
837                     }
838                     sources.add(input);
839                 }
840             } else if (arg.equals(ARG_RESOURCES)) {
841                 if (index == args.length - 1) {
842                     System.err.println("Missing resource folder name");
843                     exit(ERRNO_INVALID_ARGS);
844                 }
845                 String paths = args[++index];
846                 for (String path : Lint.splitPath(paths)) {
847                     File input = getInArgumentPath(path);
848                     if (!input.exists()) {
849                         System.err.println("Resource folder " + input + " does not exist.");
850                         exit(ERRNO_INVALID_ARGS);
851                     }
852                     List<File> resources = flags.getResourcesOverride();
853                     if (resources == null) {
854                         resources = new ArrayList<>();
855                         flags.setResourcesOverride(resources);
856                     }
857                     resources.add(input);
858                 }
859             } else if (arg.equals(ARG_LIBRARIES)) {
860                 if (index == args.length - 1) {
861                     System.err.println("Missing library folder name");
862                     exit(ERRNO_INVALID_ARGS);
863                 }
864                 String paths = args[++index];
865                 for (String path : Lint.splitPath(paths)) {
866                     File input = getInArgumentPath(path);
867                     if (!input.exists()) {
868                         System.err.println("Library " + input + " does not exist.");
869                         exit(ERRNO_INVALID_ARGS);
870                     }
871                     List<File> libraries = flags.getLibrariesOverride();
872                     if (libraries == null) {
873                         libraries = new ArrayList<>();
874                         flags.setLibrariesOverride(libraries);
875                     }
876                     libraries.add(input);
877                 }
878             } else if (arg.equals(ARG_BUILD_API)) {
879                 if (index == args.length - 1) {
880                     System.err.println("Missing compileSdkVersion");
881                     exit(ERRNO_INVALID_ARGS);
882                 }
883                 String version = args[++index];
884                 flags.setCompileSdkVersionOverride(version);
885             } else if (arg.equals(ARG_PROJECT)) {
886                 if (index == args.length - 1) {
887                     System.err.println("Missing project description file");
888                     exit(ERRNO_INVALID_ARGS);
889                 }
890                 String paths = args[++index];
891                 for (String path : Lint.splitPath(paths)) {
892                     File input = getInArgumentPath(path);
893                     if (!input.exists()) {
894                         System.err.println("Project descriptor " + input + " does not exist.");
895                         exit(ERRNO_INVALID_ARGS);
896                     }
897                     File descriptor = flags.getProjectDescriptorOverride();
898                     //noinspection VariableNotUsedInsideIf
899                     if (descriptor != null) {
900                         System.err.println("Project descriptor should only be specified once");
901                         exit(ERRNO_INVALID_ARGS);
902                     }
903                     flags.setProjectDescriptorOverride(input);
904                 }
905             } else if (arg.equals(ARG_SDK_HOME)) {
906                 if (index == args.length - 1) {
907                     System.err.println("Missing SDK home directory");
908                     exit(ERRNO_INVALID_ARGS);
909                 }
910                 sdkHome = new File(args[++index]);
911                 if (!sdkHome.isDirectory()) {
912                     System.err.println(sdkHome + " is not a directory");
913                     exit(ERRNO_INVALID_ARGS);
914                 }
915             } else if (arg.equals(ARG_BASELINE)) {
916                 if (index == args.length - 1) {
917                     System.err.println("Missing baseline file path");
918                     exit(ERRNO_INVALID_ARGS);
919                 }
920                 String path = args[++index];
921                 File input = getInArgumentPath(path);
922                 flags.setBaselineFile(input);
923             } else if (arg.equals(ARG_REMOVE_FIXED)) {
924                 if (flags.isUpdateBaseline()) {
925                     System.err.printf(
926                             Locale.US,
927                             "Cannot use both %s and %s.%n",
928                             ARG_REMOVE_FIXED,
929                             ARG_UPDATE_BASELINE);
930                 }
931                 flags.setRemovedFixedBaselineIssues(true);
932             } else if (arg.equals(ARG_UPDATE_BASELINE)) {
933                 if (flags.isRemoveFixedBaselineIssues()) {
934                     System.err.printf(
935                             Locale.US,
936                             "Cannot use both %s and %s.%n",
937                             ARG_UPDATE_BASELINE,
938                             ARG_REMOVE_FIXED);
939                 }
940                 flags.setUpdateBaseline(true);
941             } else if (arg.equals(ARG_ALLOW_SUPPRESS)) {
942                 flags.setAllowSuppress(true);
943             } else if (arg.equals(ARG_RESTRICT_SUPPRESS)) {
944                 flags.setAllowSuppress(false);
945             } else if (arg.startsWith("--")) {
946                 System.err.println("Invalid argument " + arg + "\n");
947                 printUsage(System.err);
948                 exit(ERRNO_INVALID_ARGS);
949             } else {
950                 String filename = arg;
951                 File file = getInArgumentPath(filename);
952
953                 if (!file.exists()) {
954                     System.err.println(String.format("%1$s does not exist.", filename));
955                     exit(ERRNO_EXISTS);
956                 }
957                 files.add(file);
958             }
959         }
960
961         if (files.isEmpty() && flags.getProjectDescriptorOverride() == null) {
962             System.err.println("No files to analyze.");
963             exit(ERRNO_INVALID_ARGS);
964         } else if (files.size() > 1
965                 && (flags.getClassesOverride() != null
966                         || flags.getSourcesOverride() != null
967                         || flags.getLibrariesOverride() != null
968                         || flags.getResourcesOverride() != null)) {
969             System.err.println(
970                     String.format(
971                             "The %1$s, %2$s, %3$s and %4$s arguments can only be used with a single project",
972                             ARG_SOURCES, ARG_CLASSES, ARG_LIBRARIES, ARG_RESOURCES));
973             exit(ERRNO_INVALID_ARGS);
974         }
975
976         client.syncConfigOptions();
977
978         List<Reporter> reporters = flags.getReporters();
979         if (reporters.isEmpty()) {
980             //noinspection VariableNotUsedInsideIf
981             if (urlMap != null) {
982                 System.err.println(
983                         String.format(
984                                 "Warning: The %1$s option only applies to HTML reports (%2$s)",
985                                 ARG_URL, ARG_HTML));
986             }
987
988             reporters.add(
989                     new TextReporter(client, flags, new PrintWriter(System.out, true), false));
990         } else {
991             //noinspection VariableNotUsedInsideIf
992             if (urlMap != null) {
993                 if (!urlMap.equals(VALUE_NONE)) {
994                     Map<String, String> map = new HashMap<>();
995                     String[] replace = urlMap.split(",");
996                     for (String s : replace) {
997                         // Allow ='s in the suffix part
998                         int index = s.indexOf('=');
999                         if (index == -1) {
1000                             System.err.println(
1001                                     "The URL map argument must be of the form 'path_prefix=url_prefix'");
1002                             exit(ERRNO_INVALID_ARGS);
1003                         }
1004                         String key = s.substring(0, index);
1005                         String value = s.substring(index + 1);
1006                         map.put(key, value);
1007                     }
1008                     for (Reporter reporter : reporters) {
1009                         reporter.setUrlMap(map);
1010                     }
1011                 }
1012             }
1013         }
1014
1015         try {
1016             // Not using globalIssueRegistry; LintClient will do its own registry merging
1017             // also including project rules.
1018             int exitCode = client.run(new BuiltinIssueRegistry(), files);
1019             exit(exitCode);
1020         } catch (IOException e) {
1021             log(e, null);
1022             exit(ERRNO_INVALID_ARGS);
1023         }
1024     }
1025
1026     private IssueRegistry getGlobalRegistry(LintCliClient client) {
1027         if (globalIssueRegistry == null) {
1028             globalIssueRegistry = client.addCustomLintRules(new BuiltinIssueRegistry());
1029         }
1030
1031         return globalIssueRegistry;
1032     }
1033
1034     /**
1035      * Converts a relative or absolute command-line argument into an input file.
1036      *
1037      * @param filename The filename given as a command-line argument.
1038      * @return A File matching filename, either absolute or relative to lint.workdir if defined.
1039      */
1040     private static File getInArgumentPath(String filename) {
1041         File file = new File(filename);
1042
1043         if (!file.isAbsolute()) {
1044             File workDir = getLintWorkDir();
1045             if (workDir != null) {
1046                 File file2 = new File(workDir, filename);
1047                 if (file2.exists()) {
1048                     try {
1049                         file = file2.getCanonicalFile();
1050                     } catch (IOException e) {
1051                         file = file2;
1052                     }
1053                 }
1054             }
1055         }
1056         return file;
1057     }
1058
1059     /**
1060      * Converts a relative or absolute command-line argument into an output file.
1061      *
1062      * <p>The difference with {@code getInArgumentPath} is that we can't check whether the a
1063      * relative path turned into an absolute compared to lint.workdir actually exists.
1064      *
1065      * @param filename The filename given as a command-line argument.
1066      * @return A File matching filename, either absolute or relative to lint.workdir if defined.
1067      */
1068     private static File getOutArgumentPath(String filename) {
1069         File file = new File(filename);
1070
1071         if (!file.isAbsolute()) {
1072             File workDir = getLintWorkDir();
1073             if (workDir != null) {
1074                 File file2 = new File(workDir, filename);
1075                 try {
1076                     file = file2.getCanonicalFile();
1077                 } catch (IOException e) {
1078                     file = file2;
1079                 }
1080             }
1081         }
1082         return file;
1083     }
1084
1085     /**
1086      * Returns the File corresponding to the system property or the environment variable for {@link
1087      * #PROP_WORK_DIR}. This property is typically set by the SDK/tools/lint[.bat] wrapper. It
1088      * denotes the path where the command-line client was originally invoked from and can be used to
1089      * convert relative input/output paths.
1090      *
1091      * @return A new File corresponding to {@link #PROP_WORK_DIR} or null.
1092      */
1093     @Nullable
1094     private static File getLintWorkDir() {
1095         // First check the Java properties (e.g. set using "java -jar ... -Dname=value")
1096         String path = System.getProperty(PROP_WORK_DIR);
1097         if (path == null || path.isEmpty()) {
1098             // If not found, check environment variables.
1099             path = System.getenv(PROP_WORK_DIR);
1100         }
1101         if (path != null && !path.isEmpty()) {
1102             return new File(path);
1103         }
1104         return null;
1105     }
1106
1107     private static void printHelpTopicSuppress() {
1108         System.out.println(wrap(TextFormat.RAW.convertTo(getSuppressHelp(), TextFormat.TEXT)));
1109     }
1110
1111     static String getSuppressHelp() {
1112         // \\u00a0 is a non-breaking space
1113         final String NBSP = "\u00a0\u00a0\u00a0\u00a0";
1114
1115         return "Lint errors can be suppressed in a variety of ways:\n"
1116                 + "\n"
1117                 + "1. With a `@SuppressLint` annotation in the Java code\n"
1118                 + "2. With a `tools:ignore` attribute in the XML file\n"
1119                 + "3. With a //noinspection comment in the source code\n"
1120                 + "4. With ignore flags specified in the `build.gradle` file, "
1121                 + "as explained below\n"
1122                 + "5. With a `lint.xml` configuration file in the project\n"
1123                 + "6. With a `lint.xml` configuration file passed to lint "
1124                 + "via the "
1125                 + ARG_CONFIG
1126                 + " flag\n"
1127                 + "7. With the "
1128                 + ARG_IGNORE
1129                 + " flag passed to lint.\n"
1130                 + "\n"
1131                 + "To suppress a lint warning with an annotation, add "
1132                 + "a `@SuppressLint(\"id\")` annotation on the class, method "
1133                 + "or variable declaration closest to the warning instance "
1134                 + "you want to disable. The id can be one or more issue "
1135                 + "id's, such as `\"UnusedResources\"` or `{\"UnusedResources\","
1136                 + "\"UnusedIds\"}`, or it can be `\"all\"` to suppress all lint "
1137                 + "warnings in the given scope.\n"
1138                 + "\n"
1139                 + "To suppress a lint warning with a comment, add "
1140                 + "a `//noinspection id` comment on the line before the statement "
1141                 + "with the error.\n"
1142                 + "\n"
1143                 + "To suppress a lint warning in an XML file, add a "
1144                 + "`tools:ignore=\"id\"` attribute on the element containing "
1145                 + "the error, or one of its surrounding elements. You also "
1146                 + "need to define the namespace for the tools prefix on the "
1147                 + "root element in your document, next to the `xmlns:android` "
1148                 + "declaration:\n"
1149                 + "`xmlns:tools=\"http://schemas.android.com/tools\"`\n"
1150                 + "\n"
1151                 + "To suppress a lint warning in a `build.gradle` file, add a "
1152                 + "section like this:\n"
1153                 + "\n"
1154                 + "android {\n"
1155                 + NBSP
1156                 + "lintOptions {\n"
1157                 + NBSP
1158                 + NBSP
1159                 + "disable 'TypographyFractions','TypographyQuotes'\n"
1160                 + NBSP
1161                 + "}\n"
1162                 + "}\n"
1163                 + "\n"
1164                 + "Here we specify a comma separated list of issue id's after the "
1165                 + "disable command. You can also use `warning` or `error` instead "
1166                 + "of `disable` to change the severity of issues.\n"
1167                 + "\n"
1168                 + "To suppress lint warnings with a configuration XML file, "
1169                 + "create a file named `lint.xml` and place it at the root "
1170                 + "directory of the module in which it applies.\n"
1171                 + "\n"
1172                 + "The format of the `lint.xml` file is something like the "
1173                 + "following:\n"
1174                 + "\n"
1175                 + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1176                 + "<lint>\n"
1177                 + NBSP
1178                 + "<!-- Ignore everything in the test source set -->\n"
1179                 + NBSP
1180                 + "<issue id=\"all\">\n"
1181                 + NBSP
1182                 + NBSP
1183                 + "<ignore path=\"\\*/test/\\*\" />\n"
1184                 + NBSP
1185                 + "</issue>\n"
1186                 + "\n"
1187                 + NBSP
1188                 + "<!-- Disable this given check in this project -->\n"
1189                 + NBSP
1190                 + "<issue id=\"IconMissingDensityFolder\" severity=\"ignore\" />\n"
1191                 + "\n"
1192                 + NBSP
1193                 + "<!-- Ignore the ObsoleteLayoutParam issue in the given files -->\n"
1194                 + NBSP
1195                 + "<issue id=\"ObsoleteLayoutParam\">\n"
1196                 + NBSP
1197                 + NBSP
1198                 + "<ignore path=\"res/layout/activation.xml\" />\n"
1199                 + NBSP
1200                 + NBSP
1201                 + "<ignore path=\"res/layout-xlarge/activation.xml\" />\n"
1202                 + NBSP
1203                 + NBSP
1204                 + "<ignore regexp=\"(foo|bar)\\.java\" />\n"
1205                 + NBSP
1206                 + "</issue>\n"
1207                 + "\n"
1208                 + NBSP
1209                 + "<!-- Ignore the UselessLeaf issue in the given file -->\n"
1210                 + NBSP
1211                 + "<issue id=\"UselessLeaf\">\n"
1212                 + NBSP
1213                 + NBSP
1214                 + "<ignore path=\"res/layout/main.xml\" />\n"
1215                 + NBSP
1216                 + "</issue>\n"
1217                 + "\n"
1218                 + NBSP
1219                 + "<!-- Change the severity of hardcoded strings to \"error\" -->\n"
1220                 + NBSP
1221                 + "<issue id=\"HardcodedText\" severity=\"error\" />\n"
1222                 + "</lint>\n"
1223                 + "\n"
1224                 + "To suppress lint checks from the command line, pass the "
1225                 + ARG_IGNORE
1226                 + " "
1227                 + "flag with a comma separated list of ids to be suppressed, such as:\n"
1228                 + "`$ lint --ignore UnusedResources,UselessLeaf /my/project/path`\n"
1229                 + "\n"
1230                 + "For more information, see "
1231                 + "http://g.co/androidstudio/suppressing-lint-warnings\n";
1232     }
1233
1234     private static void printVersion(LintCliClient client) {
1235         String revision = client.getClientDisplayRevision();
1236         if (revision != null) {
1237             System.out.println(String.format("lint: version %1$s", revision));
1238         } else {
1239             System.out.println("lint: unknown version");
1240         }
1241     }
1242
1243     private static void displayValidIds(IssueRegistry registry, PrintStream out) {
1244         List<Category> categories = registry.getCategories();
1245         out.println("Valid issue categories:");
1246         for (Category category : categories) {
1247             out.println("    " + category.getFullName());
1248         }
1249         out.println();
1250         List<Issue> issues = registry.getIssues();
1251         out.println("Valid issue id's:");
1252         for (Issue issue : issues) {
1253             listIssue(out, issue);
1254         }
1255     }
1256
1257     private static void listIssue(PrintStream out, Issue issue) {
1258         out.print(wrapArg("\"" + issue.getId() + "\": " + issue.getBriefDescription(TEXT)));
1259     }
1260
1261     private static void showIssues(IssueRegistry registry) {
1262         List<Issue> issues = registry.getIssues();
1263         List<Issue> sorted = new ArrayList<>(issues);
1264         sorted.sort(
1265                 (issue1, issue2) -> {
1266                     int d = issue1.getCategory().compareTo(issue2.getCategory());
1267                     if (d != 0) {
1268                         return d;
1269                     }
1270                     d = issue2.getPriority() - issue1.getPriority();
1271                     if (d != 0) {
1272                         return d;
1273                     }
1274
1275                     return issue1.getId().compareTo(issue2.getId());
1276                 });
1277
1278         System.out.println("Available issues:\n");
1279         Category previousCategory = null;
1280         for (Issue issue : sorted) {
1281             Category category = issue.getCategory();
1282             if (!category.equals(previousCategory)) {
1283                 String name = category.getFullName();
1284                 System.out.println(name);
1285                 for (int i = 0, n = name.length(); i < n; i++) {
1286                     System.out.print('=');
1287                 }
1288                 System.out.println('\n');
1289                 previousCategory = category;
1290             }
1291
1292             describeIssue(issue);
1293             System.out.println();
1294         }
1295     }
1296
1297     private static void describeIssue(Issue issue) {
1298         System.out.println(issue.getId());
1299         for (int i = 0; i < issue.getId().length(); i++) {
1300             System.out.print('-');
1301         }
1302         System.out.println();
1303         System.out.println(wrap("Summary: " + issue.getBriefDescription(TEXT)));
1304         System.out.println("Priority: " + issue.getPriority() + " / 10");
1305         System.out.println("Severity: " + issue.getDefaultSeverity().getDescription());
1306         System.out.println("Category: " + issue.getCategory().getFullName());
1307
1308         if (!issue.isEnabledByDefault()) {
1309             System.out.println("NOTE: This issue is disabled by default!");
1310             System.out.println(
1311                     String.format(
1312                             "You can enable it by adding %1$s %2$s", ARG_ENABLE, issue.getId()));
1313         }
1314
1315         System.out.println();
1316         System.out.println(wrap(issue.getExplanation(TEXT)));
1317         List<String> moreInfo = issue.getMoreInfo();
1318         if (!moreInfo.isEmpty()) {
1319             System.out.println("More information: ");
1320             for (String uri : moreInfo) {
1321                 System.out.println(uri);
1322             }
1323         }
1324     }
1325
1326     static String wrapArg(String explanation) {
1327         // Wrap arguments such that the wrapped lines are not showing up in the left column
1328         return wrap(explanation, MAX_LINE_WIDTH, "      ");
1329     }
1330
1331     static String wrap(String explanation) {
1332         return wrap(explanation, MAX_LINE_WIDTH, "");
1333     }
1334
1335     static String wrap(String explanation, int lineWidth, String hangingIndent) {
1336         return SdkUtils.wrap(explanation, lineWidth, hangingIndent);
1337     }
1338
1339     private static void printUsage(PrintStream out) {
1340         // TODO: Look up launcher script name!
1341         String command = "lint";
1342
1343         out.println("Usage: " + command + " [flags] <project directories>\n");
1344         out.println("Flags:\n");
1345
1346         printUsage(
1347                 out,
1348                 new String[] {
1349                     ARG_HELP,
1350                     "This message.",
1351                     ARG_HELP + " <topic>",
1352                     "Help on the given topic, such as \"suppress\".",
1353                     ARG_LIST_IDS,
1354                     "List the available issue id's and exit.",
1355                     ARG_VERSION,
1356                     "Output version information and exit.",
1357                     ARG_EXIT_CODE,
1358                     "Set the exit code to " + ERRNO_ERRORS + " if errors are found.",
1359                     ARG_SHOW,
1360                     "List available issues along with full explanations.",
1361                     ARG_SHOW + " <ids>",
1362                     "Show full explanations for the given list of issue id's.",
1363                     ARG_FATAL,
1364                     "Only check for fatal severity issues",
1365                     ARG_AUTO_FIX,
1366                     "Apply suggestions to the source code (for safe fixes)",
1367                     "",
1368                     "\nEnabled Checks:",
1369                     ARG_DISABLE + " <list>",
1370                     "Disable the list of categories or "
1371                             + "specific issue id's. The list should be a comma-separated list of issue "
1372                             + "id's or categories.",
1373                     ARG_ENABLE + " <list>",
1374                     "Enable the specific list of issues. "
1375                             + "This checks all the default issues plus the specifically enabled issues. The "
1376                             + "list should be a comma-separated list of issue id's or categories.",
1377                     ARG_CHECK + " <list>",
1378                     "Only check the specific list of issues. "
1379                             + "This will disable everything and re-enable the given list of issues. "
1380                             + "The list should be a comma-separated list of issue id's or categories.",
1381                     ARG_NO_WARN_1 + ", " + ARG_NO_WARN_2,
1382                     "Only check for errors (ignore warnings)",
1383                     ARG_WARN_ALL,
1384                     "Check all warnings, including those off by default",
1385                     ARG_ALL_ERROR,
1386                     "Treat all warnings as errors",
1387                     ARG_CONFIG + " <filename>",
1388                     "Use the given configuration file to "
1389                             + "determine whether issues are enabled or disabled. If a project contains "
1390                             + "a lint.xml file, then this config file will be used as a fallback.",
1391                     ARG_BASELINE,
1392                     "Use (or create) the given baseline file to filter out known issues.",
1393                     ARG_ALLOW_SUPPRESS,
1394                     "Whether to allow suppressing issues that have been explicitly registered "
1395                             + "as not suppressible.",
1396                     "",
1397                     "\nOutput Options:",
1398                     ARG_QUIET,
1399                     "Don't show progress.",
1400                     ARG_FULL_PATH,
1401                     "Use full paths in the error output.",
1402                     ARG_SHOW_ALL,
1403                     "Do not truncate long messages, lists of alternate locations, etc.",
1404                     ARG_NO_LINES,
1405                     "Do not include the source file lines with errors "
1406                             + "in the output. By default, the error output includes snippets of source code "
1407                             + "on the line containing the error, but this flag turns it off.",
1408                     ARG_HTML + " <filename>",
1409                     "Create an HTML report instead. If the filename is a "
1410                             + "directory (or a new filename without an extension), lint will create a "
1411                             + "separate report for each scanned project.",
1412                     ARG_URL + " filepath=url",
1413                     "Add links to HTML report, replacing local "
1414                             + "path prefixes with url prefix. The mapping can be a comma-separated list of "
1415                             + "path prefixes to corresponding URL prefixes, such as "
1416                             + "C:\\temp\\Proj1=http://buildserver/sources/temp/Proj1.  To turn off linking "
1417                             + "to files, use "
1418                             + ARG_URL
1419                             + " "
1420                             + VALUE_NONE,
1421                     ARG_XML + " <filename>",
1422                     "Create an XML report instead.",
1423                     "",
1424                     "\nProject Options:",
1425                     ARG_PROJECT + " <file>",
1426                     "Use the given project layout descriptor file to describe "
1427                             + "the set of available sources, resources and libraries. Used to drive lint with "
1428                             + "build systems not natively integrated with lint.",
1429                     ARG_RESOURCES + " <dir>",
1430                     "Add the given folder (or path) as a resource directory "
1431                             + "for the project. Only valid when running lint on a single project.",
1432                     ARG_SOURCES + " <dir>",
1433                     "Add the given folder (or path) as a source directory for "
1434                             + "the project. Only valid when running lint on a single project.",
1435                     ARG_CLASSES + " <dir>",
1436                     "Add the given folder (or jar file, or path) as a class "
1437                             + "directory for the project. Only valid when running lint on a single project.",
1438                     ARG_LIBRARIES + " <dir>",
1439                     "Add the given folder (or jar file, or path) as a class "
1440                             + "library for the project. Only valid when running lint on a single project.",
1441                     ARG_BUILD_API + " <version>",
1442                     "Use the given compileSdkVersion to pick an SDK "
1443                             + "target to resolve Android API call to",
1444                     ARG_SDK_HOME + " <dir>",
1445                     "Use the given SDK instead of attempting to find it "
1446                             + "relative to the lint installation or via $ANDROID_SDK_ROOT",
1447                     "",
1448                     "\nExit Status:",
1449                     "0",
1450                     "Success.",
1451                     Integer.toString(ERRNO_ERRORS),
1452                     "Lint errors detected.",
1453                     Integer.toString(ERRNO_USAGE),
1454                     "Lint usage.",
1455                     Integer.toString(ERRNO_EXISTS),
1456                     "Cannot clobber existing file.",
1457                     Integer.toString(ERRNO_HELP),
1458                     "Lint help.",
1459                     Integer.toString(ERRNO_INVALID_ARGS),
1460                     "Invalid command-line argument.",
1461                 });
1462     }
1463
1464     private static void printUsage(PrintStream out, String[] args) {
1465         int argWidth = 0;
1466         for (int i = 0; i < args.length; i += 2) {
1467             String arg = args[i];
1468             argWidth = Math.max(argWidth, arg.length());
1469         }
1470         argWidth += 2;
1471         StringBuilder sb = new StringBuilder(20);
1472         for (int i = 0; i < argWidth; i++) {
1473             sb.append(' ');
1474         }
1475         String indent = sb.toString();
1476         String formatString = "%1$-" + argWidth + "s%2$s";
1477
1478         for (int i = 0; i < args.length; i += 2) {
1479             String arg = args[i];
1480             String description = args[i + 1];
1481             if (arg.isEmpty()) {
1482                 out.println(description);
1483             } else {
1484                 out.print(
1485                         wrap(
1486                                 String.format(formatString, arg, description),
1487                                 MAX_LINE_WIDTH,
1488                                 indent));
1489             }
1490         }
1491     }
1492
1493     public void log(
1494             @Nullable Throwable exception, @Nullable String format, @Nullable Object... args) {
1495         System.out.flush();
1496         if (!flags.isQuiet()) {
1497             // Place the error message on a line of its own since we're printing '.' etc
1498             // with newlines during analysis
1499             System.err.println();
1500         }
1501         if (format != null) {
1502             System.err.println(String.format(format, args));
1503         }
1504         if (exception != null) {
1505             exception.printStackTrace();
1506         }
1507     }
1508
1509     @VisibleForTesting
1510     static final class ExitException extends RuntimeException {
1511
1512         private final int status;
1513
1514         ExitException(int status) {
1515             this.status = status;
1516         }
1517
1518         int getStatus() {
1519             return status;
1520         }
1521     }
1522
1523     private static void exit(int value) {
1524         throw new ExitException(value);
1525     }
1526 }