2 * Copyright (C) 2011 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.tools.lint;
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;
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;
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;
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;
79 * Command line driver for the lint framework
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>
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";
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";
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;
133 /** Creates a CLI driver */
137 * Runs the static analysis command line driver
139 * @param args program arguments
141 public static void main(String[] args) {
143 new Main().run(args);
144 } catch (ExitException exitException) {
145 System.exit(exitException.getStatus());
149 /** Hook intended for tests */
150 protected void initializeDriver(@NonNull LintDriver driver) {}
153 * Runs the static analysis command line driver
155 * @param args program arguments
157 @SuppressWarnings("UnnecessaryLocalVariable")
158 public void run(String[] args) {
159 if (args.length < 1) {
160 printUsage(System.err);
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) {
170 private Pattern mAndroidAnnotationPattern;
171 private Project unexpectedGradleProject = null;
175 protected LintDriver createDriver(
176 @NonNull IssueRegistry registry, @NonNull LintRequest request) {
177 LintDriver driver = super.createDriver(registry, request);
179 Project project = unexpectedGradleProject;
180 if (project != null) {
183 "\"`%1$s`\" is a Gradle project. To correctly "
184 + "analyze Gradle projects, you should run \"`gradlew :lint`\" "
188 Lint.guessGradleLocation(this, project.getDir(), null);
189 LintClient.Companion.report(
191 IssueRegistry.LINT_ERROR,
199 initializeDriver(driver);
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;
219 public Configuration getConfiguration(
220 @NonNull final Project project, @Nullable LintDriver driver) {
221 if (overrideConfiguration != null) {
222 return overrideConfiguration;
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) {
233 public Severity getSeverity(@NonNull Issue issue) {
234 return issue == IssueRegistry.LINT_ERROR
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)) {
253 return issue != IssueRegistry.LINT_ERROR;
257 return super.getConfiguration(project, driver);
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)) {
268 path.substring(srcJarIndex + 8)
269 .replace(File.separatorChar, '/');
270 ZipEntry entry = zipFile.getEntry(name);
272 try (InputStream is = zipFile.getInputStream(entry)) {
273 byte[] bytes = ByteStreams.toByteArray(is);
275 } catch (Exception e) {
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);
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);
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");
305 return mAndroidAnnotationPattern
307 .replaceAll("android.support.annotation");
315 public byte[] readBytes(@NonNull File file) throws IOException {
316 // .srcjar file handle?
317 byte[] srcJarBytes = readSrcJar(file);
318 if (srcJarBytes != null) {
322 return super.readBytes(file);
325 private ProjectMetadata metadata;
327 /** Creates a lint request */
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);
339 if (metadata.getSdk() != null) {
340 sdkHome = metadata.getSdk();
343 if (metadata.getBaseline() != null) {
344 flags.setBaselineFile(metadata.getBaseline());
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);
353 request.setScope(scope);
355 request.setPlatform(metadata.getPlatforms());
364 public List<File> findRuleJars(@NonNull Project project) {
365 if (metadata != null) {
366 List<File> jars = metadata.getLintChecks().get(project);
372 return super.findRuleJars(project);
377 public List<File> findGlobalRuleJars() {
378 if (metadata != null) {
379 List<File> jars = metadata.getGlobalLintChecks();
380 if (!jars.isEmpty()) {
385 return super.findGlobalRuleJars();
390 public File getCacheDir(@Nullable String name, boolean create) {
391 if (metadata != null) {
392 File dir = metadata.getCache();
395 dir = new File(dir, name);
398 if (create && !dir.exists()) {
407 return super.getCacheDir(name, create);
412 public Document getMergedManifest(@NonNull Project project) {
413 if (metadata != null) {
414 File manifest = metadata.getMergedManifests().get(project);
415 if (manifest != null && manifest.exists()) {
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);
427 return super.getMergedManifest(project);
432 public File getSdkHome() {
433 if (Main.this.sdkHome != null) {
434 return Main.this.sdkHome;
436 return super.getSdkHome();
440 protected boolean addBootClassPath(
441 @NonNull Collection<? extends Project> knownProjects,
443 if (metadata != null && !metadata.getJdkBootClasspath().isEmpty()) {
444 boolean isAndroid = false;
445 for (Project project : knownProjects) {
446 if (project.isAndroidProject()) {
452 files.addAll(metadata.getJdkBootClasspath());
456 boolean ok = super.addBootClassPath(knownProjects, files);
458 files.addAll(metadata.getJdkBootClasspath());
463 return super.addBootClassPath(knownProjects, files);
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());
474 return externalAnnotations;
478 // Mapping from file path prefix to URL. Applies only to HTML reports
479 String urlMap = null;
481 List<File> files = new ArrayList<>();
482 for (int index = 0; index < args.length; index++) {
483 String arg = args[index];
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();
492 System.err.println(String.format("Unknown help topic \"%1$s\"", topic));
493 exit(ERRNO_INVALID_ARGS);
496 printUsage(System.out);
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);
516 System.err.println("Invalid category \"" + id + "\".\n");
517 displayValidIds(registry, System.err);
518 exit(ERRNO_INVALID_ARGS);
522 displayValidIds(registry, System.out);
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();
543 } else if (registry.isIssueId(id)) {
544 describeIssue(registry.getIssue(id));
545 System.out.println();
547 System.err.println("Invalid id or category \"" + id + "\".\n");
548 displayValidIds(registry, System.err);
549 exit(ERRNO_INVALID_ARGS);
553 showIssues(registry);
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);
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);
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;
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);
590 File file = getInArgumentPath(args[++index]);
591 if (!file.exists()) {
592 System.err.println(file.getAbsolutePath() + " does not exist");
593 exit(ERRNO_INVALID_ARGS);
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);
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();
610 log(null, "Could not create output directory %1$s", output);
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");
619 flags.getReporters().add(reporter);
622 if (output.exists()) {
623 boolean delete = output.delete();
625 System.err.println("Could not delete old " + output);
629 if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
630 System.err.println("Cannot write HTML output file " + output);
634 Reporter reporter = Reporter.createHtmlReporter(client, output, flags);
635 flags.getReporters().add(reporter);
636 } catch (IOException e) {
638 exit(ERRNO_INVALID_ARGS);
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);
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();
650 if (output.exists()) {
651 boolean delete = output.delete();
653 System.err.println("Could not delete old " + output);
657 if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
658 System.err.println("Cannot write XML output file " + output);
664 Reporter.createXmlReporter(
665 client, output, false, flags.isIncludeXmlFixes()));
666 } catch (IOException e) {
668 exit(ERRNO_INVALID_ARGS);
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);
676 Writer writer = null;
678 String outputName = args[++index];
679 if (outputName.equals("stdout")) {
680 //noinspection IOResourceOpenedButNotSafelyClosed,resource
681 writer = new PrintWriter(System.out, true);
684 File output = getOutArgumentPath(outputName);
686 // Get an absolute path such that we can ask its parent directory for
687 // write permission etc.
688 output = output.getAbsoluteFile();
690 if (output.exists()) {
691 boolean delete = output.delete();
693 System.err.println("Could not delete old " + output);
697 if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
698 System.err.println("Cannot write text output file " + output);
702 //noinspection IOResourceOpenedButNotSafelyClosed,resource
703 writer = new BufferedWriter(new FileWriter(output));
704 } catch (IOException e) {
706 exit(ERRNO_INVALID_ARGS);
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);
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());
731 flags.getSuppressedIds().add(id);
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);
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());
751 flags.getEnabledIds().add(id);
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);
759 Set<String> checkedIds = flags.getExactCheckedIds();
760 if (checkedIds == null) {
761 checkedIds = new HashSet<>();
762 flags.setExactCheckedIds(checkedIds);
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());
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);
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);
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);
814 List<File> classes = flags.getClassesOverride();
815 if (classes == null) {
816 classes = new ArrayList<>();
817 flags.setClassesOverride(classes);
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);
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);
833 List<File> sources = flags.getSourcesOverride();
834 if (sources == null) {
835 sources = new ArrayList<>();
836 flags.setSourcesOverride(sources);
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);
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);
852 List<File> resources = flags.getResourcesOverride();
853 if (resources == null) {
854 resources = new ArrayList<>();
855 flags.setResourcesOverride(resources);
857 resources.add(input);
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);
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);
871 List<File> libraries = flags.getLibrariesOverride();
872 if (libraries == null) {
873 libraries = new ArrayList<>();
874 flags.setLibrariesOverride(libraries);
876 libraries.add(input);
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);
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);
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);
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);
903 flags.setProjectDescriptorOverride(input);
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);
910 sdkHome = new File(args[++index]);
911 if (!sdkHome.isDirectory()) {
912 System.err.println(sdkHome + " is not a directory");
913 exit(ERRNO_INVALID_ARGS);
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);
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()) {
927 "Cannot use both %s and %s.%n",
929 ARG_UPDATE_BASELINE);
931 flags.setRemovedFixedBaselineIssues(true);
932 } else if (arg.equals(ARG_UPDATE_BASELINE)) {
933 if (flags.isRemoveFixedBaselineIssues()) {
936 "Cannot use both %s and %s.%n",
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);
950 String filename = arg;
951 File file = getInArgumentPath(filename);
953 if (!file.exists()) {
954 System.err.println(String.format("%1$s does not exist.", filename));
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)) {
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);
976 client.syncConfigOptions();
978 List<Reporter> reporters = flags.getReporters();
979 if (reporters.isEmpty()) {
980 //noinspection VariableNotUsedInsideIf
981 if (urlMap != null) {
984 "Warning: The %1$s option only applies to HTML reports (%2$s)",
989 new TextReporter(client, flags, new PrintWriter(System.out, true), false));
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('=');
1001 "The URL map argument must be of the form 'path_prefix=url_prefix'");
1002 exit(ERRNO_INVALID_ARGS);
1004 String key = s.substring(0, index);
1005 String value = s.substring(index + 1);
1006 map.put(key, value);
1008 for (Reporter reporter : reporters) {
1009 reporter.setUrlMap(map);
1016 // Not using globalIssueRegistry; LintClient will do its own registry merging
1017 // also including project rules.
1018 int exitCode = client.run(new BuiltinIssueRegistry(), files);
1020 } catch (IOException e) {
1022 exit(ERRNO_INVALID_ARGS);
1026 private IssueRegistry getGlobalRegistry(LintCliClient client) {
1027 if (globalIssueRegistry == null) {
1028 globalIssueRegistry = client.addCustomLintRules(new BuiltinIssueRegistry());
1031 return globalIssueRegistry;
1035 * Converts a relative or absolute command-line argument into an input file.
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.
1040 private static File getInArgumentPath(String filename) {
1041 File file = new File(filename);
1043 if (!file.isAbsolute()) {
1044 File workDir = getLintWorkDir();
1045 if (workDir != null) {
1046 File file2 = new File(workDir, filename);
1047 if (file2.exists()) {
1049 file = file2.getCanonicalFile();
1050 } catch (IOException e) {
1060 * Converts a relative or absolute command-line argument into an output file.
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.
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.
1068 private static File getOutArgumentPath(String filename) {
1069 File file = new File(filename);
1071 if (!file.isAbsolute()) {
1072 File workDir = getLintWorkDir();
1073 if (workDir != null) {
1074 File file2 = new File(workDir, filename);
1076 file = file2.getCanonicalFile();
1077 } catch (IOException e) {
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.
1091 * @return A new File corresponding to {@link #PROP_WORK_DIR} or null.
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);
1101 if (path != null && !path.isEmpty()) {
1102 return new File(path);
1107 private static void printHelpTopicSuppress() {
1108 System.out.println(wrap(TextFormat.RAW.convertTo(getSuppressHelp(), TextFormat.TEXT)));
1111 static String getSuppressHelp() {
1112 // \\u00a0 is a non-breaking space
1113 final String NBSP = "\u00a0\u00a0\u00a0\u00a0";
1115 return "Lint errors can be suppressed in a variety of ways:\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 "
1129 + " flag passed to lint.\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"
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"
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` "
1149 + "`xmlns:tools=\"http://schemas.android.com/tools\"`\n"
1151 + "To suppress a lint warning in a `build.gradle` file, add a "
1152 + "section like this:\n"
1159 + "disable 'TypographyFractions','TypographyQuotes'\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"
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"
1172 + "The format of the `lint.xml` file is something like the "
1175 + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
1178 + "<!-- Ignore everything in the test source set -->\n"
1180 + "<issue id=\"all\">\n"
1183 + "<ignore path=\"\\*/test/\\*\" />\n"
1188 + "<!-- Disable this given check in this project -->\n"
1190 + "<issue id=\"IconMissingDensityFolder\" severity=\"ignore\" />\n"
1193 + "<!-- Ignore the ObsoleteLayoutParam issue in the given files -->\n"
1195 + "<issue id=\"ObsoleteLayoutParam\">\n"
1198 + "<ignore path=\"res/layout/activation.xml\" />\n"
1201 + "<ignore path=\"res/layout-xlarge/activation.xml\" />\n"
1204 + "<ignore regexp=\"(foo|bar)\\.java\" />\n"
1209 + "<!-- Ignore the UselessLeaf issue in the given file -->\n"
1211 + "<issue id=\"UselessLeaf\">\n"
1214 + "<ignore path=\"res/layout/main.xml\" />\n"
1219 + "<!-- Change the severity of hardcoded strings to \"error\" -->\n"
1221 + "<issue id=\"HardcodedText\" severity=\"error\" />\n"
1224 + "To suppress lint checks from the command line, pass the "
1227 + "flag with a comma separated list of ids to be suppressed, such as:\n"
1228 + "`$ lint --ignore UnusedResources,UselessLeaf /my/project/path`\n"
1230 + "For more information, see "
1231 + "http://g.co/androidstudio/suppressing-lint-warnings\n";
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));
1239 System.out.println("lint: unknown version");
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());
1250 List<Issue> issues = registry.getIssues();
1251 out.println("Valid issue id's:");
1252 for (Issue issue : issues) {
1253 listIssue(out, issue);
1257 private static void listIssue(PrintStream out, Issue issue) {
1258 out.print(wrapArg("\"" + issue.getId() + "\": " + issue.getBriefDescription(TEXT)));
1261 private static void showIssues(IssueRegistry registry) {
1262 List<Issue> issues = registry.getIssues();
1263 List<Issue> sorted = new ArrayList<>(issues);
1265 (issue1, issue2) -> {
1266 int d = issue1.getCategory().compareTo(issue2.getCategory());
1270 d = issue2.getPriority() - issue1.getPriority();
1275 return issue1.getId().compareTo(issue2.getId());
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('=');
1288 System.out.println('\n');
1289 previousCategory = category;
1292 describeIssue(issue);
1293 System.out.println();
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('-');
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());
1308 if (!issue.isEnabledByDefault()) {
1309 System.out.println("NOTE: This issue is disabled by default!");
1312 "You can enable it by adding %1$s %2$s", ARG_ENABLE, issue.getId()));
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);
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, " ");
1331 static String wrap(String explanation) {
1332 return wrap(explanation, MAX_LINE_WIDTH, "");
1335 static String wrap(String explanation, int lineWidth, String hangingIndent) {
1336 return SdkUtils.wrap(explanation, lineWidth, hangingIndent);
1339 private static void printUsage(PrintStream out) {
1340 // TODO: Look up launcher script name!
1341 String command = "lint";
1343 out.println("Usage: " + command + " [flags] <project directories>\n");
1344 out.println("Flags:\n");
1351 ARG_HELP + " <topic>",
1352 "Help on the given topic, such as \"suppress\".",
1354 "List the available issue id's and exit.",
1356 "Output version information and exit.",
1358 "Set the exit code to " + ERRNO_ERRORS + " if errors are found.",
1360 "List available issues along with full explanations.",
1361 ARG_SHOW + " <ids>",
1362 "Show full explanations for the given list of issue id's.",
1364 "Only check for fatal severity issues",
1366 "Apply suggestions to the source code (for safe fixes)",
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)",
1384 "Check all warnings, including those off by default",
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.",
1392 "Use (or create) the given baseline file to filter out known issues.",
1394 "Whether to allow suppressing issues that have been explicitly registered "
1395 + "as not suppressible.",
1397 "\nOutput Options:",
1399 "Don't show progress.",
1401 "Use full paths in the error output.",
1403 "Do not truncate long messages, lists of alternate locations, etc.",
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=https://buildserver/sources/temp/Proj1. To turn off linking "
1421 ARG_XML + " <filename>",
1422 "Create an XML report instead.",
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",
1451 Integer.toString(ERRNO_ERRORS),
1452 "Lint errors detected.",
1453 Integer.toString(ERRNO_USAGE),
1455 Integer.toString(ERRNO_EXISTS),
1456 "Cannot clobber existing file.",
1457 Integer.toString(ERRNO_HELP),
1459 Integer.toString(ERRNO_INVALID_ARGS),
1460 "Invalid command-line argument.",
1464 private static void printUsage(PrintStream out, String[] args) {
1466 for (int i = 0; i < args.length; i += 2) {
1467 String arg = args[i];
1468 argWidth = Math.max(argWidth, arg.length());
1471 StringBuilder sb = new StringBuilder(20);
1472 for (int i = 0; i < argWidth; i++) {
1475 String indent = sb.toString();
1476 String formatString = "%1$-" + argWidth + "s%2$s";
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);
1486 String.format(formatString, arg, description),
1494 @Nullable Throwable exception, @Nullable String format, @Nullable Object... args) {
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();
1501 if (format != null) {
1502 System.err.println(String.format(format, args));
1504 if (exception != null) {
1505 exception.printStackTrace();
1510 static final class ExitException extends RuntimeException {
1512 private final int status;
1514 ExitException(int status) {
1515 this.status = status;
1523 private static void exit(int value) {
1524 throw new ExitException(value);