blob: 855e0ab4701f707b1ecc07cffcd0831e8e41bff2 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.VALUE_NONE;
import static com.android.tools.lint.LintCliFlags.ERRNO_ERRORS;
import static com.android.tools.lint.LintCliFlags.ERRNO_EXISTS;
import static com.android.tools.lint.LintCliFlags.ERRNO_HELP;
import static com.android.tools.lint.LintCliFlags.ERRNO_INVALID_ARGS;
import static com.android.tools.lint.LintCliFlags.ERRNO_SUCCESS;
import static com.android.tools.lint.LintCliFlags.ERRNO_USAGE;
import static com.android.tools.lint.detector.api.Lint.endsWith;
import static com.android.tools.lint.detector.api.TextFormat.TEXT;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.DefaultConfiguration;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.LintRequest;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Lint;
import com.android.tools.lint.detector.api.LintModelModuleProject;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.tools.lint.model.LintModelModule;
import com.android.tools.lint.model.LintModelSerialization;
import com.android.tools.lint.model.LintModelVariant;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import com.intellij.openapi.util.Ref;
import com.intellij.pom.java.LanguageLevel;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.config.ApiVersion;
import org.jetbrains.kotlin.config.LanguageVersion;
import org.jetbrains.kotlin.config.LanguageVersionSettings;
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
/**
* Command line driver for the lint framework
*
* <p><b>NOTE: This is not a public or final API; if you rely on this be prepared to adjust your
* code for the next tools release.</b>
*/
@Beta
public class Main {
static final int MAX_LINE_WIDTH = 78;
private static final String ARG_ENABLE = "--enable";
private static final String ARG_DISABLE = "--disable";
private static final String ARG_CHECK = "--check";
private static final String ARG_AUTO_FIX = "--apply-suggestions";
private static final String ARG_DESCRIBE_FIXES = "--describe-suggestions";
private static final String ARG_IGNORE = "--ignore";
private static final String ARG_LIST_IDS = "--list";
private static final String ARG_SHOW = "--show";
private static final String ARG_QUIET = "--quiet";
private static final String ARG_FULL_PATH = "--fullpath";
private static final String ARG_SHOW_ALL = "--showall";
private static final String ARG_HELP = "--help";
private static final String ARG_NO_LINES = "--nolines";
private static final String ARG_HTML = "--html";
private static final String ARG_SIMPLE_HTML = "--simplehtml";
private static final String ARG_XML = "--xml";
private static final String ARG_TEXT = "--text";
private static final String ARG_CONFIG = "--config";
private static final String ARG_URL = "--url";
private static final String ARG_VERSION = "--version";
private static final String ARG_EXIT_CODE = "--exitcode";
private static final String ARG_SDK_HOME = "--sdk-home";
private static final String ARG_JDK_HOME = "--jdk-home";
private static final String ARG_FATAL = "--fatalOnly";
private static final String ARG_PROJECT = "--project";
private static final String ARG_LINT_MODEL = "--lint-model";
private static final String ARG_VARIANT = "--variant";
private static final String ARG_CLASSES = "--classpath";
private static final String ARG_SOURCES = "--sources";
private static final String ARG_RESOURCES = "--resources";
private static final String ARG_LIBRARIES = "--libraries";
private static final String ARG_BUILD_API = "--compile-sdk-version";
private static final String ARG_JAVA_LANGUAGE_LEVEL = "--java-language-level";
private static final String ARG_KOTLIN_LANGUAGE_LEVEL = "--kotlin-language-level";
private static final String ARG_BASELINE = "--baseline";
private static final String ARG_REMOVE_FIXED = "--remove-fixed";
private static final String ARG_UPDATE_BASELINE = "--update-baseline";
private static final String ARG_ALLOW_SUPPRESS = "--allow-suppress";
private static final String ARG_RESTRICT_SUPPRESS = "--restrict-suppress";
private static final String ARG_NO_WARN_2 = "--nowarn";
// GCC style flag names for options
private static final String ARG_NO_WARN_1 = "-w";
private static final String ARG_WARN_ALL = "-Wall";
private static final String ARG_ALL_ERROR = "-Werror";
private static final String PROP_WORK_DIR = "com.android.tools.lint.workdir";
private final LintCliFlags flags = new LintCliFlags();
private IssueRegistry globalIssueRegistry;
@Nullable private File sdkHome;
@Nullable private File jdkHome;
/** Creates a CLI driver */
public Main() {}
/**
* Runs the static analysis command line driver
*
* @param args program arguments
*/
public static void main(String[] args) {
try {
new Main().run(args);
} catch (ExitException exitException) {
System.exit(exitException.getStatus());
}
}
/** Hook intended for tests */
protected void initializeDriver(@NonNull LintDriver driver) {}
/**
* Runs the static analysis command line driver
*
* @param args program arguments
*/
@SuppressWarnings("UnnecessaryLocalVariable")
public void run(String[] args) {
if (args.length < 1) {
printUsage(System.err);
exit(ERRNO_USAGE);
}
Ref<LanguageLevel> javaLanguageLevel = new Ref<>(null);
Ref<LanguageVersionSettings> kotlinLanguageLevel = new Ref<>(null);
List<LintModelModule> modules = new ArrayList<>();
String variantName = null;
// When running lint from the command line, warn if the project is a Gradle project
// since those projects may have custom project configuration that the command line
// runner won't know about.
LintCliClient client =
new LintCliClient(flags, LintClient.CLIENT_CLI) {
private Pattern mAndroidAnnotationPattern;
private Project unexpectedGradleProject = null;
@Override
@NonNull
protected LintDriver createDriver(
@NonNull IssueRegistry registry, @NonNull LintRequest request) {
LintDriver driver = super.createDriver(registry, request);
Project project = unexpectedGradleProject;
if (project != null) {
String message =
String.format(
"\"`%1$s`\" is a Gradle project. To correctly "
+ "analyze Gradle projects, you should run \"`gradlew lint`\" "
+ "instead.",
project.getName());
Location location =
Lint.guessGradleLocation(this, project.getDir(), null);
LintClient.Companion.report(
this,
IssueRegistry.LINT_ERROR,
message,
driver,
project,
location,
null);
}
initializeDriver(driver);
return driver;
}
@NonNull
@Override
protected Project createProject(@NonNull File dir, @NonNull File referenceDir) {
Project project = super.createProject(dir, referenceDir);
if (project.isGradleProject()) {
// Can't report error yet; stash it here so we can report it after the
// driver has been created
unexpectedGradleProject = project;
}
return project;
}
@NonNull
@Override
public LanguageLevel getJavaLanguageLevel(@NonNull Project project) {
LanguageLevel level = javaLanguageLevel.get();
if (level != null) {
return level;
}
return super.getJavaLanguageLevel(project);
}
@NonNull
@Override
public LanguageVersionSettings getKotlinLanguageLevel(
@NonNull Project project) {
LanguageVersionSettings settings = kotlinLanguageLevel.get();
if (settings != null) {
return settings;
}
return super.getKotlinLanguageLevel(project);
}
@NonNull
@Override
public Configuration getConfiguration(
@NonNull final Project project, @Nullable LintDriver driver) {
DefaultConfiguration overrideConfiguration = getOverrideConfiguration();
if (overrideConfiguration != null) {
return overrideConfiguration;
}
if (project.isGradleProject()) {
// Don't report any issues when analyzing a Gradle project from the
// non-Gradle runner; they are likely to be false, and will hide the
// real problem reported above
//noinspection ReturnOfInnerClass
return new CliConfiguration(getConfiguration(), project, true) {
@NonNull
@Override
public Severity getSeverity(@NonNull Issue issue) {
return issue == IssueRegistry.LINT_ERROR
? Severity.FATAL
: Severity.IGNORE;
}
@Override
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// If you've deliberately ignored IssueRegistry.LINT_ERROR
// don't flag that one either
if (issue == IssueRegistry.LINT_ERROR
&& new LintCliClient(flags, LintClient.getClientName())
.isSuppressed(IssueRegistry.LINT_ERROR)) {
return true;
}
return issue != IssueRegistry.LINT_ERROR;
}
};
}
return super.getConfiguration(project, driver);
}
private byte[] readSrcJar(@NonNull File file) {
String path = file.getPath();
int srcJarIndex = path.indexOf("srcjar!");
if (srcJarIndex != -1) {
File jarFile = new File(path.substring(0, srcJarIndex + 6));
if (jarFile.exists()) {
try (ZipFile zipFile = new ZipFile(jarFile)) {
String name =
path.substring(srcJarIndex + 8)
.replace(File.separatorChar, '/');
ZipEntry entry = zipFile.getEntry(name);
if (entry != null) {
try (InputStream is = zipFile.getInputStream(entry)) {
byte[] bytes = ByteStreams.toByteArray(is);
return bytes;
} catch (Exception e) {
log(e, null);
}
}
} catch (ZipException e) {
Main.this.log(e, "Could not unzip %1$s", jarFile);
} catch (IOException e) {
Main.this.log(e, "Could not read %1$s", jarFile);
}
}
}
return null;
}
@NonNull
@Override
public CharSequence readFile(@NonNull File file) {
// .srcjar file handle?
byte[] srcJarBytes = readSrcJar(file);
if (srcJarBytes != null) {
return new String(srcJarBytes, Charsets.UTF_8);
}
CharSequence contents = super.readFile(file);
if (Project.isAospBuildEnvironment()
&& file.getPath().endsWith(SdkConstants.DOT_JAVA)) {
if (mAndroidAnnotationPattern == null) {
mAndroidAnnotationPattern = Pattern.compile("android\\.annotation");
}
return mAndroidAnnotationPattern
.matcher(contents)
.replaceAll("android.support.annotation");
} else {
return contents;
}
}
@NonNull
@Override
public byte[] readBytes(@NonNull File file) throws IOException {
// .srcjar file handle?
byte[] srcJarBytes = readSrcJar(file);
if (srcJarBytes != null) {
return srcJarBytes;
}
return super.readBytes(file);
}
private ProjectMetadata metadata;
@Override
protected void configureLintRequest(@NotNull LintRequest request) {
File descriptor = flags.getProjectDescriptorOverride();
if (descriptor != null) {
metadata = ProjectInitializerKt.computeMetadata(this, descriptor);
List<Project> projects = metadata.getProjects();
if (!projects.isEmpty()) {
request.setProjects(projects);
if (metadata.getSdk() != null) {
sdkHome = metadata.getSdk();
}
if (metadata.getJdk() != null) {
jdkHome = metadata.getJdk();
}
if (metadata.getBaseline() != null) {
flags.setBaselineFile(metadata.getBaseline());
}
EnumSet<Scope> scope = EnumSet.copyOf(Scope.ALL);
if (metadata.getIncomplete()) {
scope.remove(Scope.ALL_CLASS_FILES);
scope.remove(Scope.ALL_JAVA_FILES);
scope.remove(Scope.ALL_RESOURCE_FILES);
}
request.setScope(scope);
request.setPlatform(metadata.getPlatforms());
}
}
}
@NonNull
@Override
public Iterable<File> findRuleJars(@NonNull Project project) {
if (metadata != null) {
List<File> jars = metadata.getLintChecks().get(project);
if (jars != null) {
return jars;
}
}
return super.findRuleJars(project);
}
@NonNull
@Override
public List<File> findGlobalRuleJars() {
if (metadata != null) {
List<File> jars = metadata.getGlobalLintChecks();
if (!jars.isEmpty()) {
return jars;
}
}
return super.findGlobalRuleJars();
}
@Nullable
@Override
public File getCacheDir(@Nullable String name, boolean create) {
if (metadata != null) {
File dir = metadata.getCache();
if (dir != null) {
if (name != null) {
dir = new File(dir, name);
}
if (create && !dir.exists()) {
if (!dir.mkdirs()) {
return null;
}
}
return dir;
}
}
return super.getCacheDir(name, create);
}
@Nullable
@Override
public Document getMergedManifest(@NonNull Project project) {
if (metadata != null) {
File manifest = metadata.getMergedManifests().get(project);
if (manifest != null && manifest.exists()) {
try {
// We can't call
// resolveMergeManifestSources(document, manifestReportFile)
// here since we don't have the merging log.
return XmlUtils.parseUtfXmlFile(manifest, true);
} catch (IOException | SAXException e) {
log(e, "Could not read/parse %1$s", manifest);
}
}
}
return super.getMergedManifest(project);
}
@Nullable
@Override
public File getSdkHome() {
if (Main.this.sdkHome != null) {
return Main.this.sdkHome;
}
return super.getSdkHome();
}
@Nullable
@Override
public File getJdkHome(@Nullable Project project) {
if (Main.this.jdkHome != null) {
return Main.this.jdkHome;
}
return super.getJdkHome(project);
}
@Override
protected boolean addBootClassPath(
@NonNull Collection<? extends Project> knownProjects,
@NonNull Set<File> files) {
if (metadata != null && !metadata.getJdkBootClasspath().isEmpty()) {
boolean isAndroid = false;
for (Project project : knownProjects) {
if (project.isAndroidProject()) {
isAndroid = true;
break;
}
}
if (!isAndroid) {
files.addAll(metadata.getJdkBootClasspath());
return true;
}
boolean ok = super.addBootClassPath(knownProjects, files);
if (!ok) {
files.addAll(metadata.getJdkBootClasspath());
}
return ok;
}
return super.addBootClassPath(knownProjects, files);
}
@NonNull
@Override
public List<File> getExternalAnnotations(
@NonNull Collection<? extends Project> projects) {
List<File> externalAnnotations = super.getExternalAnnotations(projects);
if (metadata != null) {
externalAnnotations.addAll(metadata.getExternalAnnotations());
}
return externalAnnotations;
}
};
// Mapping from file path prefix to URL. Applies only to HTML reports
String urlMap = null;
List<File> files = new ArrayList<>();
for (int index = 0; index < args.length; index++) {
String arg = args[index];
if (arg.equals(ARG_HELP) || arg.equals("-h") || arg.equals("-?")) {
if (index < args.length - 1) {
String topic = args[index + 1];
if (topic.equals("suppress") || topic.equals("ignore")) {
printHelpTopicSuppress();
exit(ERRNO_HELP);
} else {
System.err.println(String.format("Unknown help topic \"%1$s\"", topic));
exit(ERRNO_INVALID_ARGS);
}
}
printUsage(System.out);
exit(ERRNO_HELP);
} else if (arg.equals(ARG_LIST_IDS)) {
IssueRegistry registry = getGlobalRegistry(client);
// Did the user provide a category list?
if (index < args.length - 1 && !args[index + 1].startsWith("-")) {
String[] ids = args[++index].split(",");
for (String id : ids) {
if (registry.isCategoryName(id)) {
// List all issues with the given category
String category = id;
for (Issue issue : registry.getIssues()) {
// Check prefix such that filtering on the "Usability" category
// will match issue category "Usability:Icons" etc.
if (issue.getCategory().getName().startsWith(category)
|| issue.getCategory().getFullName().startsWith(category)) {
listIssue(System.out, issue);
}
}
} else {
System.err.println("Invalid category \"" + id + "\".\n");
displayValidIds(registry, System.err);
exit(ERRNO_INVALID_ARGS);
}
}
} else {
displayValidIds(registry, System.out);
}
exit(ERRNO_SUCCESS);
} else if (arg.equals(ARG_SHOW)) {
IssueRegistry registry = getGlobalRegistry(client);
// Show specific issues?
if (index < args.length - 1 && !args[index + 1].startsWith("-")) {
String[] ids = args[++index].split(",");
for (String id : ids) {
if (registry.isCategoryName(id)) {
// Show all issues in the given category
String category = id;
for (Issue issue : registry.getIssues()) {
// Check prefix such that filtering on the "Usability" category
// will match issue category "Usability:Icons" etc.
if (issue.getCategory().getName().startsWith(category)
|| issue.getCategory().getFullName().startsWith(category)) {
describeIssue(issue);
System.out.println();
}
}
} else if (registry.isIssueId(id)) {
describeIssue(registry.getIssue(id));
System.out.println();
} else {
System.err.println("Invalid id or category \"" + id + "\".\n");
displayValidIds(registry, System.err);
exit(ERRNO_INVALID_ARGS);
}
}
} else {
showIssues(registry);
}
exit(ERRNO_SUCCESS);
} else if (arg.equals(ARG_FULL_PATH)
|| arg.equals(ARG_FULL_PATH + "s")) { // allow "--fullpaths" too
flags.setFullPath(true);
} else if (arg.equals(ARG_SHOW_ALL)) {
flags.setShowEverything(true);
} else if (arg.equals(ARG_QUIET) || arg.equals("-q")) {
flags.setQuiet(true);
} else if (arg.equals(ARG_NO_LINES)) {
flags.setShowSourceLines(false);
} else if (arg.equals(ARG_EXIT_CODE)) {
flags.setSetExitCode(true);
} else if (arg.equals(ARG_FATAL)) {
flags.setFatalOnly(true);
} else if (arg.equals(ARG_VERSION)) {
printVersion(client);
exit(ERRNO_SUCCESS);
} else if (arg.equals(ARG_URL)) {
if (index == args.length - 1) {
System.err.println("Missing URL mapping string");
exit(ERRNO_INVALID_ARGS);
}
String map = args[++index];
// Allow repeated usage of the argument instead of just comma list
if (urlMap != null) {
//noinspection StringConcatenationInLoop
urlMap = urlMap + ',' + map;
} else {
urlMap = map;
}
} else if (arg.equals(ARG_CONFIG)) {
if (index == args.length - 1 || !endsWith(args[index + 1], DOT_XML)) {
System.err.println("Missing XML configuration file argument");
exit(ERRNO_INVALID_ARGS);
}
File file = getInArgumentPath(args[++index]);
if (!file.exists()) {
System.err.println(file.getAbsolutePath() + " does not exist");
exit(ERRNO_INVALID_ARGS);
}
flags.setDefaultConfiguration(file);
} else if (arg.equals(ARG_HTML) || arg.equals(ARG_SIMPLE_HTML)) {
if (index == args.length - 1) {
System.err.println("Missing HTML output file name");
exit(ERRNO_INVALID_ARGS);
}
File output = getOutArgumentPath(args[++index]);
// Get an absolute path such that we can ask its parent directory for
// write permission etc.
output = output.getAbsoluteFile();
if (output.isDirectory()
|| (!output.exists() && output.getName().indexOf('.') == -1)) {
if (!output.exists()) {
boolean mkdirs = output.mkdirs();
if (!mkdirs) {
log(null, "Could not create output directory %1$s", output);
exit(ERRNO_EXISTS);
}
}
MultiProjectHtmlReporter reporter =
new MultiProjectHtmlReporter(client, output, flags);
if (arg.equals(ARG_SIMPLE_HTML)) {
System.err.println(ARG_SIMPLE_HTML + " ignored: no longer supported");
}
flags.getReporters().add(reporter);
continue;
}
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
exit(ERRNO_EXISTS);
}
}
if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
System.err.println("Cannot write HTML output file " + output);
exit(ERRNO_EXISTS);
}
try {
Reporter reporter = Reporter.createHtmlReporter(client, output, flags);
flags.getReporters().add(reporter);
} catch (IOException e) {
log(e, null);
exit(ERRNO_INVALID_ARGS);
}
} else if (arg.equals(ARG_XML)) {
if (index == args.length - 1) {
System.err.println("Missing XML output file name");
exit(ERRNO_INVALID_ARGS);
}
File output = getOutArgumentPath(args[++index]);
// Get an absolute path such that we can ask its parent directory for
// write permission etc.
output = output.getAbsoluteFile();
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
exit(ERRNO_EXISTS);
}
}
if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
System.err.println("Cannot write XML output file " + output);
exit(ERRNO_EXISTS);
}
try {
flags.getReporters()
.add(
Reporter.createXmlReporter(
client, output, false, flags.isIncludeXmlFixes()));
} catch (IOException e) {
log(e, null);
exit(ERRNO_INVALID_ARGS);
}
} else if (arg.equals(ARG_TEXT)) {
if (index == args.length - 1) {
System.err.println("Missing text output file name");
exit(ERRNO_INVALID_ARGS);
}
Writer writer = null;
boolean closeWriter;
String outputName = args[++index];
if (outputName.equals("stdout")) {
//noinspection IOResourceOpenedButNotSafelyClosed,resource
writer = new PrintWriter(System.out, true);
closeWriter = false;
} else {
File output = getOutArgumentPath(outputName);
// Get an absolute path such that we can ask its parent directory for
// write permission etc.
output = output.getAbsoluteFile();
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
exit(ERRNO_EXISTS);
}
}
if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
System.err.println("Cannot write text output file " + output);
exit(ERRNO_EXISTS);
}
try {
//noinspection IOResourceOpenedButNotSafelyClosed,resource
writer = new BufferedWriter(new FileWriter(output));
} catch (IOException e) {
log(e, null);
exit(ERRNO_INVALID_ARGS);
}
closeWriter = true;
}
flags.getReporters().add(new TextReporter(client, flags, writer, closeWriter));
} else if (arg.equals(ARG_DISABLE) || arg.equals(ARG_IGNORE)) {
if (index == args.length - 1) {
System.err.println("Missing categories or id's to disable");
exit(ERRNO_INVALID_ARGS);
}
IssueRegistry registry = getGlobalRegistry(client);
String[] ids = args[++index].split(",");
for (String id : ids) {
if (registry.isCategoryName(id)) {
// Suppress all issues with the given category
String category = id;
for (Issue issue : registry.getIssues()) {
// Check prefix such that filtering on the "Usability" category
// will match issue category "Usability:Icons" etc.
if (issue.getCategory().getName().startsWith(category)
|| issue.getCategory().getFullName().startsWith(category)) {
flags.getSuppressedIds().add(issue.getId());
}
}
} else {
flags.getSuppressedIds().add(id);
}
}
} else if (arg.equals(ARG_ENABLE)) {
if (index == args.length - 1) {
System.err.println("Missing categories or id's to enable");
exit(ERRNO_INVALID_ARGS);
}
IssueRegistry registry = getGlobalRegistry(client);
String[] ids = args[++index].split(",");
for (String id : ids) {
if (registry.isCategoryName(id)) {
// Enable all issues with the given category
String category = id;
for (Issue issue : registry.getIssues()) {
if (issue.getCategory().getName().startsWith(category)
|| issue.getCategory().getFullName().startsWith(category)) {
flags.getEnabledIds().add(issue.getId());
}
}
flags.getEnabledIds().add(id);
}
}
} else if (arg.equals(ARG_CHECK)) {
if (index == args.length - 1) {
System.err.println("Missing categories or id's to check");
exit(ERRNO_INVALID_ARGS);
}
Set<String> checkedIds = flags.getExactCheckedIds();
if (checkedIds == null) {
checkedIds = new HashSet<>();
flags.setExactCheckedIds(checkedIds);
}
IssueRegistry registry = getGlobalRegistry(client);
String[] ids = args[++index].split(",");
for (String id : ids) {
if (registry.isCategoryName(id)) {
// Check all issues with the given category
String category = id;
for (Issue issue : registry.getIssues()) {
// Check prefix such that filtering on the "Usability" category
// will match issue category "Usability:Icons" etc.
if (issue.getCategory().getName().startsWith(category)
|| issue.getCategory().getFullName().startsWith(category)) {
checkedIds.add(issue.getId());
}
}
} else {
checkedIds.add(id);
}
}
} else if (arg.equals(ARG_NO_WARN_1) || arg.equals(ARG_NO_WARN_2)) {
flags.setIgnoreWarnings(true);
} else if (arg.equals(ARG_WARN_ALL)) {
flags.setCheckAllWarnings(true);
} else if (arg.equals(ARG_ALL_ERROR)) {
flags.setWarningsAsErrors(true);
} else if (arg.equals(ARG_AUTO_FIX)) {
flags.setAutoFix(true);
} else if (arg.equals(ARG_DESCRIBE_FIXES)) {
flags.setIncludeXmlFixes(true);
// Make sure we also update any XML reporters we've *already* created before
// coming across this flag:
for (Reporter reporter : flags.getReporters()) {
if (reporter instanceof XmlReporter) {
XmlReporter xmlReporter = (XmlReporter) reporter;
if (!xmlReporter.isIntendedForBaseline()) {
xmlReporter.setIncludeFixes(true);
}
}
}
} else if (arg.equals(ARG_CLASSES)) {
if (index == args.length - 1) {
System.err.println("Missing class folder name");
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Class path entry " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
List<File> classes = flags.getClassesOverride();
if (classes == null) {
classes = new ArrayList<>();
flags.setClassesOverride(classes);
}
classes.add(input);
}
} else if (arg.equals(ARG_SOURCES)) {
if (index == args.length - 1) {
System.err.println("Missing source folder name");
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Source folder " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
List<File> sources = flags.getSourcesOverride();
if (sources == null) {
sources = new ArrayList<>();
flags.setSourcesOverride(sources);
}
sources.add(input);
}
} else if (arg.equals(ARG_RESOURCES)) {
if (index == args.length - 1) {
System.err.println("Missing resource folder name");
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Resource folder " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
List<File> resources = flags.getResourcesOverride();
if (resources == null) {
resources = new ArrayList<>();
flags.setResourcesOverride(resources);
}
resources.add(input);
}
} else if (arg.equals(ARG_LIBRARIES)) {
if (index == args.length - 1) {
System.err.println("Missing library folder name");
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Library " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
List<File> libraries = flags.getLibrariesOverride();
if (libraries == null) {
libraries = new ArrayList<>();
flags.setLibrariesOverride(libraries);
}
libraries.add(input);
}
} else if (arg.equals(ARG_BUILD_API)) {
if (index == args.length - 1) {
System.err.println("Missing compileSdkVersion");
exit(ERRNO_INVALID_ARGS);
}
String version = args[++index];
flags.setCompileSdkVersionOverride(version);
} else if (arg.equals(ARG_JAVA_LANGUAGE_LEVEL)) {
if (index == args.length - 1) {
System.err.println("Missing Java language level");
exit(ERRNO_INVALID_ARGS);
}
String version = args[++index];
LanguageLevel level = LanguageLevel.parse(version);
if (level == null) {
System.err.println("Invalid Java language level \"" + version + "\"");
exit(ERRNO_INVALID_ARGS);
}
javaLanguageLevel.set(level);
} else if (arg.equals(ARG_KOTLIN_LANGUAGE_LEVEL)) {
if (index == args.length - 1) {
System.err.println("Missing Kotlin language level");
exit(ERRNO_INVALID_ARGS);
}
String version = args[++index];
LanguageVersion languageLevel = LanguageVersion.fromVersionString(version);
if (languageLevel == null) {
System.err.println("Invalid Kotlin language level \"" + version + "\"");
exit(ERRNO_INVALID_ARGS);
}
ApiVersion apiVersion = ApiVersion.createByLanguageVersion(languageLevel);
LanguageVersionSettingsImpl settings =
new LanguageVersionSettingsImpl(languageLevel, apiVersion);
kotlinLanguageLevel.set(settings);
} else if (arg.equals(ARG_PROJECT)) {
if (index == args.length - 1) {
System.err.println("Missing project description file");
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Project descriptor " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
if (!input.isFile()) {
System.err.println(
"Project descriptor "
+ input
+ " should be an XML descriptor file"
+ (input.isDirectory() ? ", not a directory" : ""));
exit(ERRNO_INVALID_ARGS);
}
File descriptor = flags.getProjectDescriptorOverride();
//noinspection VariableNotUsedInsideIf
if (descriptor != null) {
System.err.println("Project descriptor should only be specified once");
exit(ERRNO_INVALID_ARGS);
}
flags.setProjectDescriptorOverride(input);
}
} else if (arg.equals(ARG_VARIANT)) {
if (index == args.length - 1) {
System.err.println("Missing variant name after " + ARG_VARIANT);
exit(ERRNO_INVALID_ARGS);
}
variantName = args[++index];
} else if (arg.equals(ARG_LINT_MODEL)) {
if (index == args.length - 1) {
System.err.println("Missing lint model argument after " + ARG_LINT_MODEL);
exit(ERRNO_INVALID_ARGS);
}
String paths = args[++index];
for (String path : Lint.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Lint model " + input + " does not exist.");
exit(ERRNO_INVALID_ARGS);
}
if (!input.isFile()) {
System.err.println(
"Lint model "
+ input
+ " should be an XML descriptor file"
+ (input.isDirectory() ? ", not a directory" : ""));
exit(ERRNO_INVALID_ARGS);
}
try {
LintModelSerialization reader = LintModelSerialization.INSTANCE;
LintModelModule module =
reader.readModule(
input,
Collections.emptyList(),
// TODO: Define any path variables Gradle may be setting!
true,
Collections.emptyList());
modules.add(module);
} catch (Throwable error) {
System.err.println(
"Could not deserialize "
+ input
+ " to a lint model: "
+ error.toString());
exit(ERRNO_INVALID_ARGS);
}
}
} else if (arg.equals(ARG_SDK_HOME)) {
if (index == args.length - 1) {
System.err.println("Missing SDK home directory");
exit(ERRNO_INVALID_ARGS);
}
sdkHome = new File(args[++index]);
if (!sdkHome.isDirectory()) {
System.err.println(sdkHome + " is not a directory");
exit(ERRNO_INVALID_ARGS);
}
} else if (arg.equals(ARG_JDK_HOME)) {
if (index == args.length - 1) {
System.err.println("Missing JDK home directory");
exit(ERRNO_INVALID_ARGS);
}
jdkHome = new File(args[++index]);
if (!jdkHome.isDirectory()) {
System.err.println(jdkHome + " is not a directory");
exit(ERRNO_INVALID_ARGS);
}
if (!Lint.isJreFolder(jdkHome)) {
System.err.println(jdkHome + " is not a JRE/JDK");
exit(ERRNO_INVALID_ARGS);
}
} else if (arg.equals(ARG_BASELINE)) {
if (index == args.length - 1) {
System.err.println("Missing baseline file path");
exit(ERRNO_INVALID_ARGS);
}
String path = args[++index];
File input = getInArgumentPath(path);
flags.setBaselineFile(input);
} else if (arg.equals(ARG_REMOVE_FIXED)) {
if (flags.isUpdateBaseline()) {
System.err.printf(
Locale.US,
"Cannot use both %s and %s.%n",
ARG_REMOVE_FIXED,
ARG_UPDATE_BASELINE);
}
flags.setRemovedFixedBaselineIssues(true);
} else if (arg.equals(ARG_UPDATE_BASELINE)) {
if (flags.isRemoveFixedBaselineIssues()) {
System.err.printf(
Locale.US,
"Cannot use both %s and %s.%n",
ARG_UPDATE_BASELINE,
ARG_REMOVE_FIXED);
}
flags.setUpdateBaseline(true);
} else if (arg.equals(ARG_ALLOW_SUPPRESS)) {
flags.setAllowSuppress(true);
} else if (arg.equals(ARG_RESTRICT_SUPPRESS)) {
flags.setAllowSuppress(false);
} else if (arg.startsWith("--")) {
System.err.println("Invalid argument " + arg + "\n");
printUsage(System.err);
exit(ERRNO_INVALID_ARGS);
} else {
String filename = arg;
File file = getInArgumentPath(filename);
if (!file.exists()) {
System.err.println(String.format("%1$s does not exist.", filename));
exit(ERRNO_EXISTS);
}
files.add(file);
}
}
if (files.isEmpty() && flags.getProjectDescriptorOverride() == null) {
System.err.println("No files to analyze.");
exit(ERRNO_INVALID_ARGS);
} else if (files.size() > 1
&& (flags.getClassesOverride() != null
|| flags.getSourcesOverride() != null
|| flags.getLibrariesOverride() != null
|| flags.getResourcesOverride() != null)) {
System.err.println(
String.format(
"The %1$s, %2$s, %3$s and %4$s arguments can only be used with a single project",
ARG_SOURCES, ARG_CLASSES, ARG_LIBRARIES, ARG_RESOURCES));
exit(ERRNO_INVALID_ARGS);
}
client.syncConfigOptions();
List<Reporter> reporters = flags.getReporters();
if (reporters.isEmpty()) {
//noinspection VariableNotUsedInsideIf
if (urlMap != null) {
System.err.println(
String.format(
"Warning: The %1$s option only applies to HTML reports (%2$s)",
ARG_URL, ARG_HTML));
}
reporters.add(
new TextReporter(client, flags, new PrintWriter(System.out, true), false));
} else {
if (urlMap != null && !urlMap.equals(VALUE_NONE)) {
Map<String, String> map = new HashMap<>();
String[] replace = urlMap.split(",");
for (String s : replace) {
// Allow ='s in the suffix part
int index = s.indexOf('=');
if (index == -1) {
System.err.println(
"The URL map argument must be of the form 'path_prefix=url_prefix'");
exit(ERRNO_INVALID_ARGS);
}
String key = s.substring(0, index);
String value = s.substring(index + 1);
map.put(key, value);
}
for (Reporter reporter : reporters) {
reporter.setUrlMap(map);
}
}
}
LintRequest lintRequest;
if (!modules.isEmpty()) {
if (!files.isEmpty()) {
System.err.println(
"Do not specify both files and lint models: lint models should instead include the files");
exit(ERRNO_INVALID_ARGS);
}
List<Project> projects = new ArrayList<>();
for (LintModelModule module : modules) {
File dir = module.getDir();
LintModelVariant variant = null;
if (variantName != null) {
variant = module.findVariant(variantName);
if (variant == null) {
System.err.println(
"Warning: Variant "
+ variantName
+ " not found in lint model for "
+ dir);
}
}
if (variant == null) {
variant = module.defaultVariant();
}
LintModelModuleProject project =
new LintModelModuleProject(client, dir, dir, variant, null);
client.registerProject(project.getDir(), project);
projects.add(project);
}
lintRequest = new LintRequest(client, Collections.emptyList());
lintRequest.setProjects(projects);
// TODO: What about dynamic features? See LintGradleProject#configureLintRequest
} else {
lintRequest = client.createLintRequest(files);
}
try {
// Not using globalIssueRegistry; LintClient will do its own registry merging
// also including project rules.
int exitCode = client.run(new BuiltinIssueRegistry(), lintRequest);
exit(exitCode);
} catch (IOException e) {
log(e, null);
exit(ERRNO_INVALID_ARGS);
}
}
private IssueRegistry getGlobalRegistry(LintCliClient client) {
if (globalIssueRegistry == null) {
globalIssueRegistry = client.addCustomLintRules(new BuiltinIssueRegistry());
}
return globalIssueRegistry;
}
/**
* Converts a relative or absolute command-line argument into an input file.
*
* @param filename The filename given as a command-line argument.
* @return A File matching filename, either absolute or relative to lint.workdir if defined.
*/
private static File getInArgumentPath(String filename) {
File file = new File(filename);
if (!file.isAbsolute()) {
File workDir = getLintWorkDir();
if (workDir != null) {
File file2 = new File(workDir, filename);
if (file2.exists()) {
try {
file = file2.getCanonicalFile();
} catch (IOException e) {
file = file2;
}
}
}
if (!file.isAbsolute()) {
file = file.getAbsoluteFile();
}
}
return file;
}
/**
* Converts a relative or absolute command-line argument into an output file.
*
* <p>The difference with {@code getInArgumentPath} is that we can't check whether the a
* relative path turned into an absolute compared to lint.workdir actually exists.
*
* @param filename The filename given as a command-line argument.
* @return A File matching filename, either absolute or relative to lint.workdir if defined.
*/
private static File getOutArgumentPath(String filename) {
File file = new File(filename);
if (!file.isAbsolute()) {
File workDir = getLintWorkDir();
if (workDir != null) {
File file2 = new File(workDir, filename);
try {
file = file2.getCanonicalFile();
} catch (IOException e) {
file = file2;
}
}
if (!file.isAbsolute()) {
file = file.getAbsoluteFile();
}
}
return file;
}
/**
* Returns the File corresponding to the system property or the environment variable for {@link
* #PROP_WORK_DIR}. This property is typically set by the SDK/tools/lint[.bat] wrapper. It
* denotes the path where the command-line client was originally invoked from and can be used to
* convert relative input/output paths.
*
* @return A new File corresponding to {@link #PROP_WORK_DIR} or null.
*/
@Nullable
private static File getLintWorkDir() {
// First check the Java properties (e.g. set using "java -jar ... -Dname=value")
String path = System.getProperty(PROP_WORK_DIR);
if (path == null || path.isEmpty()) {
// If not found, check environment variables.
path = System.getenv(PROP_WORK_DIR);
}
if (path != null && !path.isEmpty()) {
return new File(path);
}
return null;
}
private static void printHelpTopicSuppress() {
System.out.println(wrap(TextFormat.RAW.convertTo(getSuppressHelp(), TextFormat.TEXT)));
}
static String getSuppressHelp() {
return "Lint errors can be suppressed in a variety of ways:\n"
+ "\n"
+ "1. With a `@SuppressLint` annotation in the Java code\n"
+ "2. With a `tools:ignore` attribute in the XML file\n"
+ "3. With a //noinspection comment in the source code\n"
+ "4. With ignore flags specified in the `build.gradle` file, "
+ "as explained below\n"
+ "5. With a `lint.xml` configuration file in the project\n"
+ "6. With a `lint.xml` configuration file passed to lint "
+ "via the "
+ ARG_CONFIG
+ " flag\n"
+ "7. With the "
+ ARG_IGNORE
+ " flag passed to lint.\n"
+ "\n"
+ "To suppress a lint warning with an annotation, add "
+ "a `@SuppressLint(\"id\")` annotation on the class, method "
+ "or variable declaration closest to the warning instance "
+ "you want to disable. The id can be one or more issue "
+ "id's, such as `\"UnusedResources\"` or `{\"UnusedResources\","
+ "\"UnusedIds\"}`, or it can be `\"all\"` to suppress all lint "
+ "warnings in the given scope.\n"
+ "\n"
+ "To suppress a lint warning with a comment, add "
+ "a `//noinspection id` comment on the line before the statement "
+ "with the error.\n"
+ "\n"
+ "To suppress a lint warning in an XML file, add a "
+ "`tools:ignore=\"id\"` attribute on the element containing "
+ "the error, or one of its surrounding elements. You also "
+ "need to define the namespace for the tools prefix on the "
+ "root element in your document, next to the `xmlns:android` "
+ "declaration:\n"
+ "`xmlns:tools=\"http://schemas.android.com/tools\"`\n"
+ "\n"
+ "To suppress a lint warning in a `build.gradle` file, add a "
+ "section like this:\n"
+ "\n"
+ "```gradle\n"
+ "android {\n"
+ " lintOptions {\n"
+ " disable 'TypographyFractions','TypographyQuotes'\n"
+ " }\n"
+ "}\n"
+ "```\n"
+ "\n"
+ "Here we specify a comma separated list of issue id's after the "
+ "disable command. You can also use `warning` or `error` instead "
+ "of `disable` to change the severity of issues.\n"
+ "\n"
+ "To suppress lint warnings with a configuration XML file, "
+ "create a file named `lint.xml` and place it at the root "
+ "directory of the module in which it applies.\n"
+ "\n"
+ "The format of the `lint.xml` file is something like the "
+ "following:\n"
+ "\n"
+ "```xml\n"
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<lint>\n"
+ " <!-- Ignore everything in the test source set -->\n"
+ " <issue id=\"all\">\n"
+ " <ignore path=\"\\*/test/\\*\" />\n"
+ " </issue>\n"
+ "\n"
+ " <!-- Disable this given check in this project -->\n"
+ " <issue id=\"IconMissingDensityFolder\" severity=\"ignore\" />\n"
+ "\n"
+ " <!-- Ignore the ObsoleteLayoutParam issue in the given files -->\n"
+ " <issue id=\"ObsoleteLayoutParam\">\n"
+ " <ignore path=\"res/layout/activation.xml\" />\n"
+ " <ignore path=\"res/layout-xlarge/activation.xml\" />\n"
+ " <ignore regexp=\"(foo|bar)\\.java\" />\n"
+ " </issue>\n"
+ "\n"
+ " <!-- Ignore the UselessLeaf issue in the given file -->\n"
+ " <issue id=\"UselessLeaf\">\n"
+ " <ignore path=\"res/layout/main.xml\" />\n"
+ " </issue>\n"
+ "\n"
+ " <!-- Change the severity of hardcoded strings to \"error\" -->\n"
+ " <issue id=\"HardcodedText\" severity=\"error\" />\n"
+ "</lint>\n"
+ "```\n"
+ "\n"
+ "To suppress lint checks from the command line, pass the "
+ ARG_IGNORE
+ " "
+ "flag with a comma separated list of ids to be suppressed, such as:\n"
+ "`$ lint --ignore UnusedResources,UselessLeaf /my/project/path`\n"
+ "\n"
+ "For more information, see "
+ "https://developer.android.com/studio/write/lint.html#config\n";
}
private static void printVersion(LintCliClient client) {
String revision = client.getClientDisplayRevision();
if (revision != null) {
System.out.println(String.format("lint: version %1$s", revision));
} else {
System.out.println("lint: unknown version");
}
}
private static void displayValidIds(IssueRegistry registry, PrintStream out) {
List<Category> categories = registry.getCategories();
out.println("Valid issue categories:");
for (Category category : categories) {
out.println(" " + category.getFullName());
}
out.println();
List<Issue> issues = registry.getIssues();
out.println("Valid issue id's:");
for (Issue issue : issues) {
listIssue(out, issue);
}
}
private static void listIssue(PrintStream out, Issue issue) {
out.print(wrapArg("\"" + issue.getId() + "\": " + issue.getBriefDescription(TEXT)));
}
private static void showIssues(IssueRegistry registry) {
List<Issue> issues = registry.getIssues();
List<Issue> sorted = new ArrayList<>(issues);
sorted.sort(
(issue1, issue2) -> {
int d = issue1.getCategory().compareTo(issue2.getCategory());
if (d != 0) {
return d;
}
d = issue2.getPriority() - issue1.getPriority();
if (d != 0) {
return d;
}
return issue1.getId().compareTo(issue2.getId());
});
System.out.println("Available issues:\n");
Category previousCategory = null;
for (Issue issue : sorted) {
Category category = issue.getCategory();
if (!category.equals(previousCategory)) {
String name = category.getFullName();
System.out.println(name);
for (int i = 0, n = name.length(); i < n; i++) {
System.out.print('=');
}
System.out.println('\n');
previousCategory = category;
}
describeIssue(issue);
System.out.println();
}
}
private static void describeIssue(Issue issue) {
System.out.println(issue.getId());
for (int i = 0; i < issue.getId().length(); i++) {
System.out.print('-');
}
System.out.println();
System.out.println(wrap("Summary: " + issue.getBriefDescription(TEXT)));
System.out.println("Priority: " + issue.getPriority() + " / 10");
System.out.println("Severity: " + issue.getDefaultSeverity().getDescription());
System.out.println("Category: " + issue.getCategory().getFullName());
if (!issue.isEnabledByDefault()) {
System.out.println("NOTE: This issue is disabled by default!");
System.out.println(
String.format(
"You can enable it by adding %1$s %2$s", ARG_ENABLE, issue.getId()));
}
System.out.println();
System.out.println(wrap(issue.getExplanation(TEXT)));
List<String> moreInfo = issue.getMoreInfo();
if (!moreInfo.isEmpty()) {
System.out.println("More information: ");
for (String uri : moreInfo) {
System.out.println(uri);
}
}
}
static String wrapArg(String explanation) {
// Wrap arguments such that the wrapped lines are not showing up in the left column
return wrap(explanation, MAX_LINE_WIDTH, " ");
}
static String wrap(String explanation) {
return wrap(explanation, MAX_LINE_WIDTH, "");
}
static String wrap(String explanation, int lineWidth, String hangingIndent) {
return SdkUtils.wrap(explanation, lineWidth, hangingIndent);
}
private static void printUsage(PrintStream out) {
// TODO: Look up launcher script name!
String command = "lint";
out.println("Usage: " + command + " [flags] <project directories>\n");
out.println("Flags:\n");
printUsage(
out,
new String[] {
ARG_HELP,
"This message.",
ARG_HELP + " <topic>",
"Help on the given topic, such as \"suppress\".",
ARG_LIST_IDS,
"List the available issue id's and exit.",
ARG_VERSION,
"Output version information and exit.",
ARG_EXIT_CODE,
"Set the exit code to " + ERRNO_ERRORS + " if errors are found.",
ARG_SHOW,
"List available issues along with full explanations.",
ARG_SHOW + " <ids>",
"Show full explanations for the given list of issue id's.",
ARG_FATAL,
"Only check for fatal severity issues",
ARG_AUTO_FIX,
"Apply suggestions to the source code (for safe fixes)",
"",
"\nEnabled Checks:",
ARG_DISABLE + " <list>",
"Disable the list of categories or "
+ "specific issue id's. The list should be a comma-separated list of issue "
+ "id's or categories.",
ARG_ENABLE + " <list>",
"Enable the specific list of issues. "
+ "This checks all the default issues plus the specifically enabled issues. The "
+ "list should be a comma-separated list of issue id's or categories.",
ARG_CHECK + " <list>",
"Only check the specific list of issues. "
+ "This will disable everything and re-enable the given list of issues. "
+ "The list should be a comma-separated list of issue id's or categories.",
ARG_NO_WARN_1 + ", " + ARG_NO_WARN_2,
"Only check for errors (ignore warnings)",
ARG_WARN_ALL,
"Check all warnings, including those off by default",
ARG_ALL_ERROR,
"Treat all warnings as errors",
ARG_CONFIG + " <filename>",
"Use the given configuration file to "
+ "determine whether issues are enabled or disabled. If a project contains "
+ "a lint.xml file, then this config file will be used as a fallback.",
ARG_BASELINE,
"Use (or create) the given baseline file to filter out known issues.",
ARG_ALLOW_SUPPRESS,
"Whether to allow suppressing issues that have been explicitly registered "
+ "as not suppressible.",
"",
"\nOutput Options:",
ARG_QUIET,
"Don't show progress.",
ARG_FULL_PATH,
"Use full paths in the error output.",
ARG_SHOW_ALL,
"Do not truncate long messages, lists of alternate locations, etc.",
ARG_NO_LINES,
"Do not include the source file lines with errors "
+ "in the output. By default, the error output includes snippets of source code "
+ "on the line containing the error, but this flag turns it off.",
ARG_HTML + " <filename>",
"Create an HTML report instead. If the filename is a "
+ "directory (or a new filename without an extension), lint will create a "
+ "separate report for each scanned project.",
ARG_URL + " filepath=url",
"Add links to HTML report, replacing local "
+ "path prefixes with url prefix. The mapping can be a comma-separated list of "
+ "path prefixes to corresponding URL prefixes, such as "
+ "C:\\temp\\Proj1=http://buildserver/sources/temp/Proj1. To turn off linking "
+ "to files, use "
+ ARG_URL
+ " "
+ VALUE_NONE,
ARG_XML + " <filename>",
"Create an XML report instead.",
ARG_TEXT + " <filename>",
"Write a text report to the given file. If the filename is just `stdout` (short "
+ "for standard out), the report is written to the console.",
"",
"\nProject Options:",
ARG_PROJECT + " <file>",
"Use the given project layout descriptor file to describe "
+ "the set of available sources, resources and libraries. Used to drive lint with "
+ "build systems not natively integrated with lint.",
ARG_RESOURCES + " <dir>",
"Add the given folder (or path) as a resource directory "
+ "for the project. Only valid when running lint on a single project.",
ARG_SOURCES + " <dir>",
"Add the given folder (or path) as a source directory for "
+ "the project. Only valid when running lint on a single project.",
ARG_CLASSES + " <dir>",
"Add the given folder (or jar file, or path) as a class "
+ "directory for the project. Only valid when running lint on a single project.",
ARG_LIBRARIES + " <dir>",
"Add the given folder (or jar file, or path) as a class "
+ "library for the project. Only valid when running lint on a single project.",
ARG_BUILD_API + " <version>",
"Use the given compileSdkVersion to pick an SDK "
+ "target to resolve Android API call to",
ARG_SDK_HOME + " <dir>",
"Use the given SDK instead of attempting to find it "
+ "relative to the lint installation or via $ANDROID_SDK_ROOT",
ARG_JDK_HOME + " <dir>",
"Use the given JDK instead of attempting to find it via $JAVA_HOME or java.home",
ARG_JAVA_LANGUAGE_LEVEL + " <level>",
"Use the given version of the Java programming language",
ARG_KOTLIN_LANGUAGE_LEVEL + " <level>",
"Use the given version of the Kotlin programming language",
"",
"\nExit Status:",
"0",
"Success.",
Integer.toString(ERRNO_ERRORS),
"Lint errors detected.",
Integer.toString(ERRNO_USAGE),
"Lint usage.",
Integer.toString(ERRNO_EXISTS),
"Cannot clobber existing file.",
Integer.toString(ERRNO_HELP),
"Lint help.",
Integer.toString(ERRNO_INVALID_ARGS),
"Invalid command-line argument.",
});
}
private static void printUsage(PrintStream out, String[] args) {
int argWidth = 0;
for (int i = 0; i < args.length; i += 2) {
String arg = args[i];
argWidth = Math.max(argWidth, arg.length());
}
argWidth += 2;
StringBuilder sb = new StringBuilder(20);
for (int i = 0; i < argWidth; i++) {
sb.append(' ');
}
String indent = sb.toString();
String formatString = "%1$-" + argWidth + "s%2$s";
for (int i = 0; i < args.length; i += 2) {
String arg = args[i];
String description = args[i + 1];
if (arg.isEmpty()) {
out.println(description);
} else {
out.print(
wrap(
String.format(formatString, arg, description),
MAX_LINE_WIDTH,
indent));
}
}
}
public void log(
@Nullable Throwable exception, @Nullable String format, @Nullable Object... args) {
System.out.flush();
if (!flags.isQuiet()) {
// Place the error message on a line of its own since we're printing '.' etc
// with newlines during analysis
System.err.println();
}
if (format != null) {
System.err.println(String.format(format, args));
}
if (exception != null) {
exception.printStackTrace();
}
}
@VisibleForTesting
static final class ExitException extends RuntimeException {
private final int status;
ExitException(int status) {
this.status = status;
}
int getStatus() {
return status;
}
}
private static void exit(int value) {
throw new ExitException(value);
}
}