blob: c53469ed1ec5ee0886be5a12dad6350e08cffcec [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.tools.lint.client.api.IssueRegistry.LINT_ERROR;
import static com.android.tools.lint.client.api.IssueRegistry.PARSER_ERROR;
import static com.android.tools.lint.detector.api.LintUtils.endsWith;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
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.IDomParser;
import com.android.tools.lint.client.api.IJavaParser;
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.LintListener;
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.LintUtils;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Position;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.Closeables;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
/**
* Command line driver for the lint framework
*/
public class Main extends LintClient {
static final int MAX_LINE_WIDTH = 78;
private static final String ARG_ENABLE = "--enable"; //$NON-NLS-1$
private static final String ARG_DISABLE = "--disable"; //$NON-NLS-1$
private static final String ARG_CHECK = "--check"; //$NON-NLS-1$
private static final String ARG_IGNORE = "--ignore"; //$NON-NLS-1$
private static final String ARG_LISTIDS = "--list"; //$NON-NLS-1$
private static final String ARG_SHOW = "--show"; //$NON-NLS-1$
private static final String ARG_QUIET = "--quiet"; //$NON-NLS-1$
private static final String ARG_FULLPATH = "--fullpath"; //$NON-NLS-1$
private static final String ARG_SHOWALL = "--showall"; //$NON-NLS-1$
private static final String ARG_HELP = "--help"; //$NON-NLS-1$
private static final String ARG_NOLINES = "--nolines"; //$NON-NLS-1$
private static final String ARG_HTML = "--html"; //$NON-NLS-1$
private static final String ARG_SIMPLEHTML = "--simplehtml"; //$NON-NLS-1$
private static final String ARG_XML = "--xml"; //$NON-NLS-1$
private static final String ARG_TEXT = "--text"; //$NON-NLS-1$
private static final String ARG_CONFIG = "--config"; //$NON-NLS-1$
private static final String ARG_URL = "--url"; //$NON-NLS-1$
private static final String ARG_VERSION = "--version"; //$NON-NLS-1$
private static final String ARG_EXITCODE = "--exitcode"; //$NON-NLS-1$
private static final String ARG_CLASSES = "--classpath"; //$NON-NLS-1$
private static final String ARG_SOURCES = "--sources"; //$NON-NLS-1$
private static final String ARG_RESOURCES = "--resources"; //$NON-NLS-1$
private static final String ARG_LIBRARIES = "--libraries"; //$NON-NLS-1$
private static final String ARG_NOWARN2 = "--nowarn"; //$NON-NLS-1$
// GCC style flag names for options
private static final String ARG_NOWARN1 = "-w"; //$NON-NLS-1$
private static final String ARG_WARNALL = "-Wall"; //$NON-NLS-1$
private static final String ARG_ALLERROR = "-Werror"; //$NON-NLS-1$
private static final String VALUE_NONE = "none"; //$NON-NLS-1$
private static final String PROP_WORK_DIR = "com.android.tools.lint.workdir"; //$NON-NLS-1$
private static final int ERRNO_ERRORS = 1;
private static final int ERRNO_USAGE = 2;
private static final int ERRNO_EXISTS = 3;
private static final int ERRNO_HELP = 4;
private static final int ERRNO_INVALIDARGS = 5;
protected final List<Warning> mWarnings = new ArrayList<Warning>();
protected final Set<String> mSuppress = new HashSet<String>();
protected final Set<String> mEnabled = new HashSet<String>();
/** If non-null, only run the specified checks (possibly modified by enable/disables) */
protected Set<String> mCheck = null;
protected boolean mHasErrors;
protected boolean mSetExitCode;
protected boolean mFullPath;
protected int mErrorCount;
protected int mWarningCount;
protected boolean mShowLines = true;
protected final List<Reporter> mReporters = Lists.newArrayList();
protected boolean mQuiet;
protected boolean mWarnAll;
protected boolean mNoWarnings;
protected boolean mAllErrors;
protected List<File> mSources;
protected List<File> mClasses;
protected List<File> mLibraries;
protected List<File> mResources;
protected Configuration mDefaultConfiguration;
protected IssueRegistry mRegistry;
protected LintDriver mDriver;
protected boolean mShowAll;
/** Creates a CLI driver */
public Main() {
}
/**
* Runs the static analysis command line driver
*
* @param args program arguments
*/
public static void main(String[] args) {
new Main().run(args);
}
/**
* Runs the static analysis command line driver
*
* @param args program arguments
*/
public void run(String[] args) {
if (args.length < 1) {
printUsage(System.err);
System.exit(ERRNO_USAGE);
}
IssueRegistry registry = mRegistry = new BuiltinIssueRegistry();
// Mapping from file path prefix to URL. Applies only to HTML reports
String urlMap = null;
List<File> files = new ArrayList<File>();
for (int index = 0; index < args.length; index++) {
String arg = args[index];
if (arg.equals(ARG_HELP)
|| arg.equals("-h") || arg.equals("-?")) { //$NON-NLS-1$ //$NON-NLS-2$
if (index < args.length - 1) {
String topic = args[index + 1];
if (topic.equals("suppress") || topic.equals("ignore")) {
printHelpTopicSuppress();
System.exit(ERRNO_HELP);
} else {
System.err.println(String.format("Unknown help topic \"%1$s\"", topic));
System.exit(ERRNO_INVALIDARGS);
}
}
printUsage(System.out);
System.exit(ERRNO_HELP);
} else if (arg.equals(ARG_LISTIDS)) {
// Did the user provide a category list?
if (index < args.length - 1 && !args[index + 1].startsWith("-")) { //$NON-NLS-1$
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);
System.exit(ERRNO_INVALIDARGS);
}
}
} else {
displayValidIds(registry, System.out);
}
System.exit(0);
} else if (arg.equals(ARG_SHOW)) {
// Show specific issues?
if (index < args.length - 1 && !args[index + 1].startsWith("-")) { //$NON-NLS-1$
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);
System.exit(ERRNO_INVALIDARGS);
}
}
} else {
showIssues(registry);
}
System.exit(0);
} else if (arg.equals(ARG_FULLPATH)
|| arg.equals(ARG_FULLPATH + "s")) { // allow "--fullpaths" too
mFullPath = true;
} else if (arg.equals(ARG_SHOWALL)) {
mShowAll = true;
} else if (arg.equals(ARG_QUIET) || arg.equals("-q")) {
mQuiet = true;
} else if (arg.equals(ARG_NOLINES)) {
mShowLines = false;
} else if (arg.equals(ARG_EXITCODE)) {
mSetExitCode = true;
} else if (arg.equals(ARG_VERSION)) {
printVersion();
System.exit(0);
} else if (arg.equals(ARG_URL)) {
if (index == args.length - 1) {
System.err.println("Missing URL mapping string");
System.exit(ERRNO_INVALIDARGS);
}
String map = args[++index];
// Allow repeated usage of the argument instead of just comma list
if (urlMap != null) {
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");
System.exit(ERRNO_INVALIDARGS);
}
File file = getInArgumentPath(args[++index]);
if (!file.exists()) {
System.err.println(file.getAbsolutePath() + " does not exist");
System.exit(ERRNO_INVALIDARGS);
}
mDefaultConfiguration = new CliConfiguration(file);
} else if (arg.equals(ARG_HTML) || arg.equals(ARG_SIMPLEHTML)) {
if (index == args.length - 1) {
System.err.println("Missing HTML output file name");
System.exit(ERRNO_INVALIDARGS);
}
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);
System.exit(ERRNO_EXISTS);
}
}
try {
MultiProjectHtmlReporter reporter =
new MultiProjectHtmlReporter(this, output);
if (arg.equals(ARG_SIMPLEHTML)) {
reporter.setSimpleFormat(true);
}
mReporters.add(reporter);
} catch (IOException e) {
log(e, null);
System.exit(ERRNO_INVALIDARGS);
}
continue;
}
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
System.exit(ERRNO_EXISTS);
}
}
if (output.getParentFile() != null && !output.getParentFile().canWrite()) {
System.err.println("Cannot write HTML output file " + output);
System.exit(ERRNO_EXISTS);
}
try {
HtmlReporter htmlReporter = new HtmlReporter(this, output);
if (arg.equals(ARG_SIMPLEHTML)) {
htmlReporter.setSimpleFormat(true);
}
mReporters.add(htmlReporter);
} catch (IOException e) {
log(e, null);
System.exit(ERRNO_INVALIDARGS);
}
} else if (arg.equals(ARG_XML)) {
if (index == args.length - 1) {
System.err.println("Missing XML output file name");
System.exit(ERRNO_INVALIDARGS);
}
File output = getOutArgumentPath(args[++index]);
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
System.exit(ERRNO_EXISTS);
}
}
if (output.canWrite()) {
System.err.println("Cannot write XML output file " + output);
System.exit(ERRNO_EXISTS);
}
try {
mReporters.add(new XmlReporter(this, output));
} catch (IOException e) {
log(e, null);
System.exit(ERRNO_INVALIDARGS);
}
} else if (arg.equals(ARG_TEXT)) {
if (index == args.length - 1) {
System.err.println("Missing XML output file name");
System.exit(ERRNO_INVALIDARGS);
}
Writer writer = null;
boolean closeWriter;
String outputName = args[++index];
if (outputName.equals("stdout")) { //$NON-NLS-1$
writer = new PrintWriter(System.out, true);
closeWriter = false;
} else {
File output = getOutArgumentPath(outputName);
if (output.exists()) {
boolean delete = output.delete();
if (!delete) {
System.err.println("Could not delete old " + output);
System.exit(ERRNO_EXISTS);
}
}
if (output.canWrite()) {
System.err.println("Cannot write XML output file " + output);
System.exit(ERRNO_EXISTS);
}
try {
writer = new BufferedWriter(new FileWriter(output));
} catch (IOException e) {
log(e, null);
System.exit(ERRNO_INVALIDARGS);
}
closeWriter = true;
}
mReporters.add(new TextReporter(this, 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");
System.exit(ERRNO_INVALIDARGS);
}
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)) {
mSuppress.add(issue.getId());
}
}
} else if (!registry.isIssueId(id)) {
System.err.println("Invalid id or category \"" + id + "\".\n");
displayValidIds(registry, System.err);
System.exit(ERRNO_INVALIDARGS);
} else {
mSuppress.add(id);
}
}
} else if (arg.equals(ARG_ENABLE)) {
if (index == args.length - 1) {
System.err.println("Missing categories or id's to enable");
System.exit(ERRNO_INVALIDARGS);
}
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)) {
mEnabled.add(issue.getId());
}
}
} else if (!registry.isIssueId(id)) {
System.err.println("Invalid id or category \"" + id + "\".\n");
displayValidIds(registry, System.err);
System.exit(ERRNO_INVALIDARGS);
} else {
mEnabled.add(id);
}
}
} else if (arg.equals(ARG_CHECK)) {
if (index == args.length - 1) {
System.err.println("Missing categories or id's to check");
System.exit(ERRNO_INVALIDARGS);
}
mCheck = new HashSet<String>();
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)) {
mCheck.add(issue.getId());
}
}
} else if (!registry.isIssueId(id)) {
System.err.println("Invalid id or category \"" + id + "\".\n");
displayValidIds(registry, System.err);
System.exit(ERRNO_INVALIDARGS);
} else {
mCheck.add(id);
}
}
} else if (arg.equals(ARG_NOWARN1) || arg.equals(ARG_NOWARN2)) {
mNoWarnings = true;
} else if (arg.equals(ARG_WARNALL)) {
mWarnAll = true;
} else if (arg.equals(ARG_ALLERROR)) {
mAllErrors = true;
} else if (arg.equals(ARG_CLASSES)) {
if (index == args.length - 1) {
System.err.println("Missing class folder name");
System.exit(ERRNO_INVALIDARGS);
}
String paths = args[++index];
for (String path : LintUtils.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Class path entry " + input + " does not exist.");
System.exit(ERRNO_INVALIDARGS);
}
if (mClasses == null) {
mClasses = new ArrayList<File>();
}
mClasses.add(input);
}
} else if (arg.equals(ARG_SOURCES)) {
if (index == args.length - 1) {
System.err.println("Missing source folder name");
System.exit(ERRNO_INVALIDARGS);
}
String paths = args[++index];
for (String path : LintUtils.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Source folder " + input + " does not exist.");
System.exit(ERRNO_INVALIDARGS);
}
if (mSources == null) {
mSources = new ArrayList<File>();
}
mSources.add(input);
}
} else if (arg.equals(ARG_RESOURCES)) {
if (index == args.length - 1) {
System.err.println("Missing resource folder name");
System.exit(ERRNO_INVALIDARGS);
}
String paths = args[++index];
for (String path : LintUtils.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Resource folder " + input + " does not exist.");
System.exit(ERRNO_INVALIDARGS);
}
if (mResources == null) {
mResources = new ArrayList<File>();
}
mResources.add(input);
}
} else if (arg.equals(ARG_LIBRARIES)) {
if (index == args.length - 1) {
System.err.println("Missing library folder name");
System.exit(ERRNO_INVALIDARGS);
}
String paths = args[++index];
for (String path : LintUtils.splitPath(paths)) {
File input = getInArgumentPath(path);
if (!input.exists()) {
System.err.println("Library " + input + " does not exist.");
System.exit(ERRNO_INVALIDARGS);
}
if (mLibraries == null) {
mLibraries = new ArrayList<File>();
}
mLibraries.add(input);
}
} else if (arg.startsWith("--")) {
System.err.println("Invalid argument " + arg + "\n");
printUsage(System.err);
System.exit(ERRNO_INVALIDARGS);
} else {
String filename = arg;
File file = getInArgumentPath(filename);
if (!file.exists()) {
System.err.println(String.format("%1$s does not exist.", filename));
System.exit(ERRNO_EXISTS);
}
files.add(file);
}
}
if (files.isEmpty()) {
System.err.println("No files to analyze.");
System.exit(ERRNO_INVALIDARGS);
} else if (files.size() > 1
&& (mClasses != null || mSources != null || mLibraries != null
|| mResources != 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));
System.exit(ERRNO_INVALIDARGS);
}
if (mReporters.isEmpty()) {
if (urlMap != null) {
System.err.println(String.format(
"Warning: The %1$s option only applies to HTML reports (%2$s)",
ARG_URL, ARG_HTML));
}
mReporters.add(new TextReporter(this, new PrintWriter(System.out, true), false));
} else {
if (urlMap == null) {
// By default just map from /foo to file:///foo
// TODO: Find out if we need file:// on Windows.
urlMap = "=file://"; //$NON-NLS-1$
} else {
for (Reporter reporter : mReporters) {
if (!reporter.isSimpleFormat()) {
reporter.setBundleResources(true);
}
}
}
if (!urlMap.equals(VALUE_NONE)) {
Map<String, String> map = new HashMap<String, String>();
String[] replace = urlMap.split(","); //$NON-NLS-1$
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'");
System.exit(ERRNO_INVALIDARGS);
}
String key = s.substring(0, index);
String value = s.substring(index + 1);
map.put(key, value);
}
for (Reporter reporter : mReporters) {
reporter.setUrlMap(map);
}
}
}
mDriver = new LintDriver(registry, this);
mDriver.setAbbreviating(!mShowAll);
if (!mQuiet) {
mDriver.addLintListener(new ProgressPrinter());
}
mDriver.analyze(files, null /* scope */);
Collections.sort(mWarnings);
for (Reporter reporter : mReporters) {
try {
reporter.write(mErrorCount, mWarningCount, mWarnings);
} catch (IOException e) {
log(e, null);
System.exit(ERRNO_INVALIDARGS);
}
}
System.exit(mSetExitCode ? (mHasErrors ? ERRNO_ERRORS : 0) : 0);
}
/**
* 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;
}
}
}
}
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;
}
}
}
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(getSuppressHelp()));
}
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 lint.xml configuration file in the project\n" +
"4. With a lint.xml configuration file passed to lint " +
"via the " + ARG_CONFIG + " flag\n" +
"5. 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 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 lint warnings with a configuration XML file, " +
"create a file named lint.xml and place it at the root " +
"directory of the project in which it applies. (If you " +
"use the Eclipse plugin's Lint view, you can suppress " +
"errors there via the toolbar and Eclipse will create the " +
"lint.xml file for you.).\n" +
"\n" +
"The format of the lint.xml file is something like the " +
"following:\n" +
"\n" +
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<lint>\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" +
" </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" +
"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";
}
private void printVersion() {
String revision = getRevision();
if (revision != null) {
System.out.println(String.format("lint: version %1$s", revision));
} else {
System.out.println("lint: unknown version");
}
}
@SuppressWarnings("resource") // Eclipse doesn't know about Closeables.closeQuietly
@Nullable
String getRevision() {
File file = findResource("tools" + File.separator + //$NON-NLS-1$
"source.properties"); //$NON-NLS-1$
if (file != null && file.exists()) {
FileInputStream input = null;
try {
input = new FileInputStream(file);
Properties properties = new Properties();
properties.load(input);
String revision = properties.getProperty("Pkg.Revision"); //$NON-NLS-1$
if (revision != null && !revision.isEmpty()) {
return revision;
}
} catch (IOException e) {
// Couldn't find or read the version info: just print out unknown below
} finally {
Closeables.closeQuietly(input);
}
}
return null;
}
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.getDescription()));
}
private static void showIssues(IssueRegistry registry) {
List<Issue> issues = registry.getIssues();
List<Issue> sorted = new ArrayList<Issue>(issues);
Collections.sort(sorted, new Comparator<Issue>() {
@Override
public int compare(Issue issue1, Issue 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.getDescription()));
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()));
}
if (issue.getExplanation() != null) {
System.out.println();
System.out.println(wrap(issue.getExplanationAsSimpleText()));
}
if (issue.getMoreInfo() != null) {
System.out.println("More information: " + issue.getMoreInfo());
}
}
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"; //$NON-NLS-1$
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_LISTIDS, "List the available issue id's and exit.",
ARG_VERSION, "Output version information and exit.",
ARG_EXITCODE, "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.",
"", "\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_NOWARN1 + ", " + ARG_NOWARN2, "Only check for errors (ignore warnings)",
ARG_WARNALL, "Check all warnings, including those off by default",
ARG_ALLERROR, "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.",
"", "\nOutput Options:",
ARG_QUIET, "Don't show progress.",
ARG_FULLPATH, "Use full paths in the error output.",
ARG_SHOWALL, "Do not truncate long messages, lists of alternate locations, etc.",
ARG_NOLINES, "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_SIMPLEHTML + " <filename>", "Create a simple HTML report",
ARG_XML + " <filename>", "Create an XML report instead.",
"", "\nProject Options:",
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.",
"", "\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_INVALIDARGS), "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"; //$NON-NLS-1$
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));
}
}
}
@Override
public void log(
@NonNull Severity severity,
@Nullable Throwable exception,
@Nullable String format,
@Nullable Object... args) {
System.out.flush();
if (!mQuiet) {
// 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();
}
}
@Override
public IDomParser getDomParser() {
return new LintCliXmlParser();
}
@Override
public Configuration getConfiguration(@NonNull Project project) {
return new CliConfiguration(mDefaultConfiguration, project);
}
/** File content cache */
private final Map<File, String> mFileContents = new HashMap<File, String>(100);
/** Read the contents of the given file, possibly cached */
private String getContents(File file) {
String s = mFileContents.get(file);
if (s == null) {
s = readFile(file);
mFileContents.put(file, s);
}
return s;
}
@Override
public IJavaParser getJavaParser() {
return new LombokParser();
}
@Override
public void report(
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@Nullable Location location,
@NonNull String message,
@Nullable Object data) {
assert context.isEnabled(issue);
if (severity == Severity.IGNORE) {
return;
}
if (severity == Severity.ERROR || severity == Severity.FATAL) {
mHasErrors = true;
mErrorCount++;
} else {
mWarningCount++;
}
Warning warning = new Warning(issue, message, severity, context.getProject(), data);
mWarnings.add(warning);
if (location != null) {
warning.location = location;
File file = location.getFile();
if (file != null) {
warning.file = file;
warning.path = getDisplayPath(context.getProject(), file);
}
Position startPosition = location.getStart();
if (startPosition != null) {
int line = startPosition.getLine();
warning.line = line;
warning.offset = startPosition.getOffset();
if (line >= 0) {
if (context.file == location.getFile()) {
warning.fileContents = context.getContents();
}
if (warning.fileContents == null) {
warning.fileContents = getContents(location.getFile());
}
if (mShowLines) {
// Compute error line contents
warning.errorLine = getLine(warning.fileContents, line);
if (warning.errorLine != null) {
// Replace tabs with spaces such that the column
// marker (^) lines up properly:
warning.errorLine = warning.errorLine.replace('\t', ' ');
int column = startPosition.getColumn();
if (column < 0) {
column = 0;
for (int i = 0; i < warning.errorLine.length(); i++, column++) {
if (!Character.isWhitespace(warning.errorLine.charAt(i))) {
break;
}
}
}
StringBuilder sb = new StringBuilder(100);
sb.append(warning.errorLine);
sb.append('\n');
for (int i = 0; i < column; i++) {
sb.append(' ');
}
boolean displayCaret = true;
Position endPosition = location.getEnd();
if (endPosition != null) {
int endLine = endPosition.getLine();
int endColumn = endPosition.getColumn();
if (endLine == line && endColumn > column) {
for (int i = column; i < endColumn; i++) {
sb.append('~');
}
displayCaret = false;
}
}
if (displayCaret) {
sb.append('^');
}
sb.append('\n');
warning.errorLine = sb.toString();
}
}
}
}
}
}
/** Look up the contents of the given line */
static String getLine(String contents, int line) {
int index = getLineOffset(contents, line);
if (index != -1) {
return getLineOfOffset(contents, index);
} else {
return null;
}
}
static String getLineOfOffset(String contents, int offset) {
int end = contents.indexOf('\n', offset);
if (end == -1) {
end = contents.indexOf('\r', offset);
}
return contents.substring(offset, end != -1 ? end : contents.length());
}
/** Look up the contents of the given line */
static int getLineOffset(String contents, int line) {
int index = 0;
for (int i = 0; i < line; i++) {
index = contents.indexOf('\n', index);
if (index == -1) {
return -1;
}
index++;
}
return index;
}
@NonNull
@Override
public String readFile(@NonNull File file) {
try {
return LintUtils.getEncodedString(this, file);
} catch (IOException e) {
return ""; //$NON-NLS-1$
}
}
boolean isCheckingSpecificIssues() {
return mCheck != null;
}
private Map<Project, ClassPathInfo> mProjectInfo;
@Override
@NonNull
protected ClassPathInfo getClassPath(@NonNull Project project) {
ClassPathInfo classPath = super.getClassPath(project);
if (mClasses == null && mSources == null && mLibraries == null) {
return classPath;
}
ClassPathInfo info;
if (mProjectInfo == null) {
mProjectInfo = Maps.newHashMap();
info = null;
} else {
info = mProjectInfo.get(project);
}
if (info == null) {
List<File> sources;
if (mSources != null) {
sources = mSources;
} else {
sources = classPath.getSourceFolders();
}
List<File> classes;
if (mClasses != null) {
classes = mClasses;
} else {
classes = classPath.getClassFolders();
}
List<File> libraries;
if (mLibraries != null) {
libraries = mLibraries;
} else {
libraries = classPath.getLibraries();
}
info = new ClassPathInfo(sources, classes, libraries);
mProjectInfo.put(project, info);
}
return info;
}
@NonNull
@Override
public List<File> getResourceFolders(@NonNull Project project) {
if (mResources == null) {
return super.getResourceFolders(project);
}
return mResources;
}
/**
* Consult the lint.xml file, but override with the --enable and --disable
* flags supplied on the command line
*/
class CliConfiguration extends DefaultConfiguration {
CliConfiguration(@NonNull Configuration parent, @NonNull Project project) {
super(Main.this, project, parent);
}
CliConfiguration(File lintFile) {
super(Main.this, null /*project*/, null /*parent*/, lintFile);
}
@NonNull
@Override
public Severity getSeverity(@NonNull Issue issue) {
Severity severity = computeSeverity(issue);
if (mAllErrors && severity != Severity.IGNORE) {
severity = Severity.ERROR;
}
if (mNoWarnings && severity == Severity.WARNING) {
severity = Severity.IGNORE;
}
return severity;
}
@NonNull
@Override
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (mWarnAll) {
return issue.getDefaultSeverity();
}
return super.getDefaultSeverity(issue);
}
private Severity computeSeverity(@NonNull Issue issue) {
Severity severity = super.getSeverity(issue);
String id = issue.getId();
if (mSuppress.contains(id)) {
return Severity.IGNORE;
}
if (mEnabled.contains(id) || (mCheck != null && mCheck.contains(id))) {
// Overriding default
// Detectors shouldn't be returning ignore as a default severity,
// but in case they do, force it up to warning here to ensure that
// it's run
if (severity == Severity.IGNORE) {
severity = issue.getDefaultSeverity();
if (severity == Severity.IGNORE) {
severity = Severity.WARNING;
}
}
return severity;
}
if (mCheck != null && issue != LINT_ERROR && issue != PARSER_ERROR) {
return Severity.IGNORE;
}
return severity;
}
}
private static class ProgressPrinter implements LintListener {
@Override
public void update(
@NonNull LintDriver lint,
@NonNull EventType type,
@Nullable Context context) {
switch (type) {
case SCANNING_PROJECT: {
String name = context != null ? context.getProject().getName() : "?";
if (lint.getPhase() > 1) {
System.out.print(String.format(
"\nScanning %1$s (Phase %2$d): ",
name,
lint.getPhase()));
} else {
System.out.print(String.format(
"\nScanning %1$s: ",
name));
}
break;
}
case SCANNING_LIBRARY_PROJECT: {
String name = context != null ? context.getProject().getName() : "?";
System.out.print(String.format(
"\n - %1$s: ",
name));
break;
}
case SCANNING_FILE:
System.out.print('.');
break;
case NEW_PHASE:
// Ignored for now: printing status as part of next project's status
break;
case CANCELED:
case COMPLETED:
System.out.println();
break;
case STARTING:
// Ignored for now
break;
}
}
}
/**
* Given a file, it produces a cleaned up path from the file.
* This will clean up the path such that
* <ul>
* <li> {@code foo/./bar} becomes {@code foo/bar}
* <li> {@code foo/bar/../baz} becomes {@code foo/baz}
* </ul>
*
* Unlike {@link File#getCanonicalPath()} however, it will <b>not</b> attempt
* to make the file canonical, such as expanding symlinks and network mounts.
*
* @param file the file to compute a clean path for
* @return the cleaned up path
*/
@VisibleForTesting
@NonNull
static String getCleanPath(@NonNull File file) {
String path = file.getPath();
StringBuilder sb = new StringBuilder(path.length());
if (path.startsWith(File.separator)) {
sb.append(File.separator);
}
elementLoop:
for (String element : Splitter.on(File.separatorChar).omitEmptyStrings().split(path)) {
if (element.equals(".")) { //$NON-NLS-1$
continue;
} else if (element.equals("..")) { //$NON-NLS-1$
if (sb.length() > 0) {
for (int i = sb.length() - 1; i >= 0; i--) {
char c = sb.charAt(i);
if (c == File.separatorChar) {
sb.setLength(i == 0 ? 1 : i);
continue elementLoop;
}
}
sb.setLength(0);
continue;
}
}
if (sb.length() > 1) {
sb.append(File.separatorChar);
} else if (sb.length() > 0 && sb.charAt(0) != File.separatorChar) {
sb.append(File.separatorChar);
}
sb.append(element);
}
if (path.endsWith(File.separator) && sb.length() > 0
&& sb.charAt(sb.length() - 1) != File.separatorChar) {
sb.append(File.separator);
}
return sb.toString();
}
String getDisplayPath(Project project, File file) {
String path = file.getPath();
if (!mFullPath && path.startsWith(project.getReferenceDir().getPath())) {
int chop = project.getReferenceDir().getPath().length();
if (path.length() > chop && path.charAt(chop) == File.separatorChar) {
chop++;
}
path = path.substring(chop);
if (path.isEmpty()) {
path = file.getName();
}
} else if (mFullPath) {
path = getCleanPath(file.getAbsoluteFile());
}
return path;
}
/** Returns whether all warnings are enabled, including those disabled by default */
boolean isAllEnabled() {
return mWarnAll;
}
/** Returns the issue registry used by this client */
IssueRegistry getRegistry() {
return mRegistry;
}
/** Returns the driver running the lint checks */
LintDriver getDriver() {
return mDriver;
}
/** Returns the configuration used by this client */
Configuration getConfiguration() {
return mDefaultConfiguration;
}
/** Returns true if the given issue has been explicitly disabled */
boolean isSuppressed(Issue issue) {
return mSuppress.contains(issue.getId());
}
}