Lint: copy diagnostics from IDEA (br143)

This commit is contained in:
Yan Zhulanow
2016-03-31 17:34:50 +03:00
parent 92d2e7a0a9
commit 126cc778e4
185 changed files with 66319 additions and 0 deletions
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2012 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.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.google.common.annotations.Beta;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Specialized visitor for running detectors on a class object model.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant method name to a list of detectors to consult for that method
* name. The set of method names that a detector is interested in is provided
* by the detectors themselves.
* <li> Second, it iterates over the DOM a single time. For each method call it finds,
* it dispatches to any check that has registered interest in that method name.
* <li> Finally, it runs a full check on those class scanners that do not register
* specific method names to be checked. This is intended for those detectors
* that do custom work, not related specifically to method calls.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <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
class AsmVisitor {
/**
* Number of distinct node types specified in {@link AbstractInsnNode}. Sadly
* there isn't a max-constant there, so update this along with ASM library
* updates.
*/
private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1;
private final Map<String, List<ClassScanner>> mMethodNameToChecks =
new HashMap<String, List<ClassScanner>>();
private final Map<String, List<ClassScanner>> mMethodOwnerToChecks =
new HashMap<String, List<ClassScanner>>();
private final List<Detector> mFullClassChecks = new ArrayList<Detector>();
private final List<? extends Detector> mAllDetectors;
private List<ClassScanner>[] mNodeTypeDetectors;
// Really want this:
//<T extends List<Detector> & Detector.ClassScanner> ClassVisitor(T xmlDetectors) {
// but it makes client code tricky and ugly.
@SuppressWarnings("unchecked")
AsmVisitor(@NonNull LintClient client, @NonNull List<? extends Detector> classDetectors) {
mAllDetectors = classDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : classDetectors) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
boolean checkFullClass = true;
Collection<String> names = scanner.getApplicableCallNames();
if (names != null) {
checkFullClass = false;
for (String element : names) {
List<Detector.ClassScanner> list = mMethodNameToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodNameToChecks.put(element, list);
}
list.add(scanner);
}
}
Collection<String> owners = scanner.getApplicableCallOwners();
if (owners != null) {
checkFullClass = false;
for (String element : owners) {
List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element);
if (list == null) {
list = new ArrayList<Detector.ClassScanner>();
mMethodOwnerToChecks.put(element, list);
}
list.add(scanner);
}
}
int[] types = scanner.getApplicableAsmNodeTypes();
if (types != null) {
checkFullClass = false;
for (int type : types) {
if (type < 0 || type >= TYPE_COUNT) {
// Can't support this node type: looks like ASM wasn't updated correctly.
client.log(null, "Out of range node type %1$d from detector %2$s",
type, scanner);
continue;
}
if (mNodeTypeDetectors == null) {
mNodeTypeDetectors = new List[TYPE_COUNT];
}
List<ClassScanner> checks = mNodeTypeDetectors[type];
if (checks == null) {
checks = new ArrayList<ClassScanner>();
mNodeTypeDetectors[type] = checks;
}
checks.add(scanner);
}
}
if (checkFullClass) {
mFullClassChecks.add(detector);
}
}
}
@SuppressWarnings("rawtypes") // ASM API uses raw types
void runClassDetectors(ClassContext context) {
ClassNode classNode = context.getClassNode();
for (Detector detector : mAllDetectors) {
detector.beforeCheckFile(context);
}
for (Detector detector : mFullClassChecks) {
Detector.ClassScanner scanner = (Detector.ClassScanner) detector;
scanner.checkClass(context, classNode);
detector.afterCheckFile(context);
}
if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() ||
mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) {
List methodList = classNode.methods;
for (Object m : methodList) {
MethodNode method = (MethodNode) m;
InsnList nodes = method.instructions;
for (int i = 0, n = nodes.size(); i < n; i++) {
AbstractInsnNode instruction = nodes.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode call = (MethodInsnNode) instruction;
String owner = call.owner;
List<ClassScanner> scanners = mMethodOwnerToChecks.get(owner);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
String name = call.name;
scanners = mMethodNameToChecks.get(name);
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkCall(context, classNode, method, call);
}
}
}
if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) {
List<ClassScanner> scanners = mNodeTypeDetectors[type];
if (scanners != null) {
for (ClassScanner scanner : scanners) {
scanner.checkInstruction(context, classNode, method, instruction);
}
}
}
}
}
}
for (Detector detector : mAllDetectors) {
detector.afterCheckFile(context);
}
}
}
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2012 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.google.common.annotations.Beta;
/**
* Exception thrown when there is a circular dependency, such as a circular dependency
* of library mProject references
* <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 CircularDependencyException extends RuntimeException {
@Nullable
private Project mProject;
@Nullable
private Location mLocation;
CircularDependencyException(@NonNull String message) {
super(message);
}
/**
* Returns the associated project, if any
*
* @return the associated project, if any
*/
@Nullable
public Project getProject() {
return mProject;
}
/**
* Sets the associated project, if any
*
* @param project the associated project, if any
*/
public void setProject(@Nullable Project project) {
mProject = project;
}
/**
* Returns the associated location, if any
*
* @return the associated location, if any
*/
@Nullable
public Location getLocation() {
return mLocation;
}
/**
* Sets the associated location, if any
*
* @param location the associated location, if any
*/
public void setLocation(@Nullable Location location) {
mLocation = location;
}
}
@@ -0,0 +1,335 @@
/*
* Copyright (C) 2015 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.client.api;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.objectweb.asm.Opcodes.ASM5;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import com.google.common.io.Closeables;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
public final File file;
public final File jarFile;
public final File binDir;
public final byte[] bytes;
@VisibleForTesting
ClassEntry(
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes) {
super();
this.file = file;
this.jarFile = jarFile;
this.binDir = binDir;
this.bytes = bytes;
}
@NonNull
public String path() {
if (jarFile != null) {
return jarFile.getPath() + ':' + file.getPath();
} else {
return file.getPath();
}
}
@Override
public int compareTo(@NonNull ClassEntry other) {
String p1 = file.getPath();
String p2 = other.file.getPath();
int m1 = p1.length();
int m2 = p2.length();
if (m1 == m2 && p1.equals(p2)) {
return 0;
}
int m = Math.min(m1, m2);
for (int i = 0; i < m; i++) {
char c1 = p1.charAt(i);
char c2 = p2.charAt(i);
if (c1 != c2) {
// Sort Foo$Bar.class *after* Foo.class, even though $ < .
if (c1 == '.' && c2 == '$') {
return -1;
}
if (c1 == '$' && c2 == '.') {
return 1;
}
return c1 - c2;
}
}
return (m == m1) ? -1 : 1;
}
@Override
public String toString() {
return file.getPath();
}
/**
* Creates a list of class entries from the given class path.
*
* @param client the client to report errors to and to use to read files
* @param classPath the class path (directories and jar files) to scan
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassPath(
@NonNull LintClient client,
@NonNull List<File> classPath,
boolean sort) {
if (!classPath.isEmpty()) {
List<ClassEntry> libraryEntries = new ArrayList<ClassEntry>(64);
addEntries(client, libraryEntries, classPath);
if (sort) {
Collections.sort(libraryEntries);
}
return libraryEntries;
} else {
return Collections.emptyList();
}
}
/**
* Creates a list of class entries from the given class path and specific set of
* files within it.
*
* @param client the client to report errors to and to use to read files
* @param classFiles the specific set of class files to look for
* @param classFolders the list of class folders to look in (to determine the
* package root)
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassFiles(
@NonNull LintClient client,
@NonNull List<File> classFiles, @NonNull List<File> classFolders,
boolean sort) {
List<ClassEntry> entries = new ArrayList<ClassEntry>(classFiles.size());
if (!classFolders.isEmpty()) {
for (File file : classFiles) {
String path = file.getPath();
if (file.isFile() && path.endsWith(DOT_CLASS)) {
try {
byte[] bytes = client.readBytes(file);
for (File dir : classFolders) {
if (path.startsWith(dir.getPath())) {
entries.add(new ClassEntry(file, null /* jarFile*/, dir,
bytes));
break;
}
}
} catch (IOException e) {
client.log(e, null);
}
}
}
if (sort && !entries.isEmpty()) {
Collections.sort(entries);
}
}
return entries;
}
/**
* Given a classpath, add all the class files found within the directories and inside jar files
*/
private static void addEntries(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries,
@NonNull List<File> classPath) {
for (File classPathEntry : classPath) {
if (classPathEntry.getName().endsWith(DOT_JAR)) {
//noinspection UnnecessaryLocalVariable
File jarFile = classPathEntry;
if (!jarFile.exists()) {
continue;
}
ZipInputStream zis = null;
try {
FileInputStream fis = new FileInputStream(jarFile);
try {
zis = new ZipInputStream(fis);
ZipEntry entry = zis.getNextEntry();
while (entry != null) {
String name = entry.getName();
if (name.endsWith(DOT_CLASS)) {
try {
byte[] bytes = ByteStreams.toByteArray(zis);
if (bytes != null) {
File file = new File(entry.getName());
entries.add(new ClassEntry(file, jarFile, jarFile, bytes));
}
} catch (Exception e) {
client.log(e, null);
continue;
}
}
entry = zis.getNextEntry();
}
} finally {
Closeables.close(fis, true);
}
} catch (IOException e) {
client.log(e, "Could not read jar file contents from %1$s", jarFile);
} finally {
try {
Closeables.close(zis, true);
} catch (IOException e) {
// cannot happen
}
}
} else if (classPathEntry.isDirectory()) {
//noinspection UnnecessaryLocalVariable
File binDir = classPathEntry;
List<File> classFiles = new ArrayList<File>();
addClassFiles(binDir, classFiles);
for (File file : classFiles) {
try {
byte[] bytes = client.readBytes(file);
entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes));
} catch (IOException e) {
client.log(e, null);
}
}
} else {
client.log(null, "Ignoring class path entry %1$s", classPathEntry);
}
}
}
/** Adds in all the .class files found recursively in the given directory */
private static void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) {
// Process the resource folder
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(DOT_CLASS)) {
classFiles.add(file);
} else if (file.isDirectory()) {
// Recurse
addClassFiles(file, classFiles);
}
}
}
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param libraryEntries the set of library entries to consult
* @param classEntries the set of class entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> libraryEntries,
@NonNull List<ClassEntry> classEntries) {
int size = libraryEntries.size() + classEntries.size();
Map<String, String> map = Maps.newHashMapWithExpectedSize(size);
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, libraryEntries);
addSuperClasses(client, visitor, classEntries);
return map;
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param entries the set of library entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries) {
Map<String, String> map = Maps.newHashMapWithExpectedSize(entries.size());
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, entries);
return map;
}
/** Adds in all the super classes found for the given class entries into the given map */
private static void addSuperClasses(
@NonNull LintClient client,
@NonNull SuperclassVisitor visitor,
@NonNull List<ClassEntry> entries) {
for (ClassEntry entry : entries) {
try {
ClassReader reader = new ClassReader(entry.bytes);
int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG
| ClassReader.SKIP_FRAMES;
reader.accept(visitor, flags);
} catch (Throwable t) {
client.log(null, "Error processing %1$s: broken class file?", entry.path());
}
}
}
/** Visitor skimming classes and initializing a map of super classes */
private static class SuperclassVisitor extends ClassVisitor {
private final Map<String, String> mMap;
public SuperclassVisitor(Map<String, String> map) {
super(ASM5);
mMap = map;
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
// Record super class in the map (but don't waste space on java.lang.Object)
if (superName != null && !"java/lang/Object".equals(superName)) {
mMap.put(name, superName);
}
}
}
}
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2013 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.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Issue;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Registry which merges many issue registries into one, and presents a unified list
* of issues.
* <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>
*/
class CompositeIssueRegistry extends IssueRegistry {
private final List<IssueRegistry> myRegistries;
private List<Issue> myIssues;
public CompositeIssueRegistry(@NonNull List<IssueRegistry> registries) {
myRegistries = registries;
}
@NonNull
@Override
public List<Issue> getIssues() {
if (myIssues == null) {
List<Issue> issues = Lists.newArrayListWithExpectedSize(200);
for (IssueRegistry registry : myRegistries) {
issues.addAll(registry.getIssues());
}
myIssues = issues;
}
return myIssues;
}
}
@@ -0,0 +1,124 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.annotations.Beta;
/**
* Lint configuration for an Android project such as which specific rules to include,
* which specific rules to exclude, and which specific errors to ignore.
* <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 abstract class Configuration {
/**
* Checks whether this issue should be ignored because the user has already
* suppressed the error? Note that this refers to individual issues being
* suppressed/ignored, not a whole detector being disabled via something
* like {@link #isEnabled(Issue)}.
*
* @param context the context used by the detector when the issue was found
* @param issue the issue that was found
* @param location the location of the issue
* @param message the associated user message
* @return true if this issue should be suppressed
*/
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
return false;
}
/**
* Returns false if the given issue has been disabled. This is just
* a convenience method for {@code getSeverity(issue) != Severity.IGNORE}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return getSeverity(issue) != Severity.IGNORE;
}
/**
* Returns the severity for a given issue. This is the same as the
* {@link Issue#getDefaultSeverity()} unless the user has selected a custom
* severity (which is tool context dependent).
*
* @param issue the issue to look up the severity from
* @return the severity use for issues for the given detector
*/
public Severity getSeverity(@NonNull Issue issue) {
return issue.getDefaultSeverity();
}
// Editing configurations
/**
* Marks the given warning as "ignored".
*
* @param context The scanning context
* @param issue the issue to be ignored
* @param location The location to ignore the warning at, if any
* @param message The message for the warning
*/
public abstract void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message);
/**
* Sets the severity to be used for this issue.
*
* @param issue the issue to set the severity for
* @param severity the severity to associate with this issue, or null to
* reset the severity to the default
*/
public abstract void setSeverity(@NonNull Issue issue, @Nullable Severity severity);
// Bulk editing support
/**
* Marks the beginning of a "bulk" editing operation with repeated calls to
* {@link #setSeverity} or {@link #ignore}. After all the values have been
* set, the client <b>must</b> call {@link #finishBulkEditing()}. This
* allows configurations to avoid doing expensive I/O (such as writing out a
* config XML file) for each and every editing operation when they are
* applied in bulk, such as from a configuration dialog's "Apply" action.
*/
public void startBulkEditing() {
}
/**
* Marks the end of a "bulk" editing operation, where values should be
* committed to persistent storage. See {@link #startBulkEditing()} for
* details.
*/
public void finishBulkEditing() {
}
}
@@ -0,0 +1,609 @@
/*
* 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.client.api;
import static com.android.SdkConstants.CURRENT_PLATFORM;
import static com.android.SdkConstants.PLATFORM_WINDOWS;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXParseException;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
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.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Default implementation of a {@link Configuration} which reads and writes
* configuration data into {@code lint.xml} in the project directory.
* <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 DefaultConfiguration extends Configuration {
private final LintClient mClient;
/** Default name of the configuration file */
public static final String CONFIG_FILE_NAME = "lint.xml"; //$NON-NLS-1$
// Lint XML File
@NonNull
private static final String TAG_ISSUE = "issue"; //$NON-NLS-1$
@NonNull
private static final String ATTR_ID = "id"; //$NON-NLS-1$
@NonNull
private static final String ATTR_SEVERITY = "severity"; //$NON-NLS-1$
@NonNull
private static final String ATTR_PATH = "path"; //$NON-NLS-1$
@NonNull
private static final String ATTR_REGEXP = "regexp"; //$NON-NLS-1$
@NonNull
private static final String TAG_IGNORE = "ignore"; //$NON-NLS-1$
@NonNull
private static final String VALUE_ALL = "all"; //$NON-NLS-1$
private final Configuration mParent;
private final Project mProject;
private final File mConfigFile;
private boolean mBulkEditing;
/** Map from id to list of project-relative paths for suppressed warnings */
private Map<String, List<String>> mSuppressed;
/** Map from id to regular expressions. */
@Nullable
private Map<String, List<Pattern>> mRegexps;
/**
* Map from id to custom {@link Severity} override
*/
private Map<String, Severity> mSeverity;
protected DefaultConfiguration(
@NonNull LintClient client,
@Nullable Project project,
@Nullable Configuration parent,
@NonNull File configFile) {
mClient = client;
mProject = project;
mParent = parent;
mConfigFile = configFile;
}
protected DefaultConfiguration(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
this(client, project, parent, new File(project.getDir(), CONFIG_FILE_NAME));
}
/**
* Creates a new {@link DefaultConfiguration}
*
* @param client the client to report errors to etc
* @param project the associated project
* @param parent the parent/fallback configuration or null
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
return new DefaultConfiguration(client, project, parent);
}
/**
* Creates a new {@link DefaultConfiguration} for the given lint config
* file, not affiliated with a project. This is used for global
* configurations.
*
* @param client the client to report errors to etc
* @param lintFile the lint file containing the configuration
* @return a new configuration
*/
@NonNull
public static DefaultConfiguration create(@NonNull LintClient client, @NonNull File lintFile) {
return new DefaultConfiguration(client, null /*project*/, null /*parent*/, lintFile);
}
@Override
public boolean isIgnored(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
ensureInitialized();
String id = issue.getId();
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = mSuppressed.get(VALUE_ALL);
}
if (paths != null && location != null) {
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
for (String suppressedPath : paths) {
if (suppressedPath.equals(relativePath)) {
return true;
}
// Also allow a prefix
if (relativePath.startsWith(suppressedPath)) {
return true;
}
}
}
if (mRegexps != null) {
List<Pattern> regexps = mRegexps.get(id);
if (regexps == null) {
regexps = mRegexps.get(VALUE_ALL);
}
if (regexps != null && location != null) {
// Check message
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(message);
if (matcher.find()) {
return true;
}
}
// Check location
File file = location.getFile();
String relativePath = context.getProject().getRelativePath(file);
boolean checkUnixPath = false;
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
} else if (regexp.pattern().indexOf('/') != -1) {
checkUnixPath = true;
}
}
if (checkUnixPath && CURRENT_PLATFORM == PLATFORM_WINDOWS) {
relativePath = relativePath.replace('\\', '/');
for (Pattern regexp : regexps) {
Matcher matcher = regexp.matcher(relativePath);
if (matcher.find()) {
return true;
}
}
}
}
}
return mParent != null && mParent.isIgnored(context, issue, location, message);
}
@NonNull
protected Severity getDefaultSeverity(@NonNull Issue issue) {
if (!issue.isEnabledByDefault()) {
return Severity.IGNORE;
}
return issue.getDefaultSeverity();
}
@Override
@NonNull
public Severity getSeverity(@NonNull Issue issue) {
ensureInitialized();
Severity severity = mSeverity.get(issue.getId());
if (severity == null) {
severity = mSeverity.get(VALUE_ALL);
}
if (severity != null) {
return severity;
}
if (mParent != null) {
return mParent.getSeverity(issue);
}
return getDefaultSeverity(issue);
}
private void ensureInitialized() {
if (mSuppressed == null) {
readConfig();
}
}
private void formatError(String message, Object... args) {
if (args != null && args.length > 0) {
message = String.format(message, args);
}
message = "Failed to parse `lint.xml` configuration file: " + message;
LintDriver driver = new LintDriver(new IssueRegistry() {
@Override @NonNull public List<Issue> getIssues() {
return Collections.emptyList();
}
}, mClient);
mClient.report(new Context(driver, mProject, mProject, mConfigFile),
IssueRegistry.LINT_ERROR,
mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR),
Location.create(mConfigFile), message, TextFormat.RAW);
}
private void readConfig() {
mSuppressed = new HashMap<String, List<String>>();
mSeverity = new HashMap<String, Severity>();
if (!mConfigFile.exists()) {
return;
}
try {
Document document = XmlUtils.parseUtfXmlFile(mConfigFile, false);
NodeList issues = document.getElementsByTagName(TAG_ISSUE);
Splitter splitter = Splitter.on(',').trimResults().omitEmptyStrings();
for (int i = 0, count = issues.getLength(); i < count; i++) {
Node node = issues.item(i);
Element element = (Element) node;
String idList = element.getAttribute(ATTR_ID);
if (idList.isEmpty()) {
formatError("Invalid lint config file: Missing required issue id attribute");
continue;
}
Iterable<String> ids = splitter.split(idList);
NamedNodeMap attributes = node.getAttributes();
for (int j = 0, n = attributes.getLength(); j < n; j++) {
Node attribute = attributes.item(j);
String name = attribute.getNodeName();
String value = attribute.getNodeValue();
if (ATTR_ID.equals(name)) {
// already handled
} else if (ATTR_SEVERITY.equals(name)) {
for (Severity severity : Severity.values()) {
if (value.equalsIgnoreCase(severity.name())) {
for (String id : ids) {
mSeverity.put(id, severity);
}
break;
}
}
} else {
formatError("Unexpected attribute \"%1$s\"", name);
}
}
// Look up ignored errors
NodeList childNodes = element.getChildNodes();
if (childNodes.getLength() > 0) {
for (int j = 0, n = childNodes.getLength(); j < n; j++) {
Node child = childNodes.item(j);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element ignore = (Element) child;
String path = ignore.getAttribute(ATTR_PATH);
if (path.isEmpty()) {
String regexp = ignore.getAttribute(ATTR_REGEXP);
if (regexp.isEmpty()) {
formatError("Missing required attribute %1$s or %2$s under %3$s",
ATTR_PATH, ATTR_REGEXP, idList);
} else {
addRegexp(idList, ids, n, regexp, false);
}
} else {
// Normalize path format to File.separator. Also
// handle the file format containing / or \.
if (File.separatorChar == '/') {
path = path.replace('\\', '/');
} else {
path = path.replace('/', File.separatorChar);
}
if (path.indexOf('*') != -1) {
String regexp = globToRegexp(path);
addRegexp(idList, ids, n, regexp, false);
} else {
for (String id : ids) {
List<String> paths = mSuppressed.get(id);
if (paths == null) {
paths = new ArrayList<String>(n / 2 + 1);
mSuppressed.put(id, paths);
}
paths.add(path);
}
}
}
}
}
}
}
} catch (SAXParseException e) {
formatError(e.getMessage());
} catch (Exception e) {
mClient.log(e, null);
}
}
@VisibleForTesting
@NonNull
public static String globToRegexp(@NonNull String glob) {
StringBuilder sb = new StringBuilder(glob.length() * 2);
int begin = 0;
sb.append('^');
for (int i = 0, n = glob.length(); i < n; i++) {
char c = glob.charAt(i);
if (c == '*') {
begin = appendQuoted(sb, glob, begin, i) + 1;
if (i < n - 1 && glob.charAt(i + 1) == '*') {
i++;
begin++;
}
sb.append(".*?");
} else if (c == '?') {
begin = appendQuoted(sb, glob, begin, i) + 1;
sb.append(".?");
}
}
appendQuoted(sb, glob, begin, glob.length());
sb.append('$');
return sb.toString();
}
private static int appendQuoted(StringBuilder sb, String s, int from, int to) {
if (to > from) {
boolean isSimple = true;
for (int i = from; i < to; i++) {
char c = s.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '/' && c != ' ') {
isSimple = false;
break;
}
}
if (isSimple) {
for (int i = from; i < to; i++) {
sb.append(s.charAt(i));
}
return to;
}
sb.append(Pattern.quote(s.substring(from, to)));
}
return to;
}
private void addRegexp(@NonNull String idList, @NonNull Iterable<String> ids, int n,
@NonNull String regexp, boolean silent) {
try {
if (mRegexps == null) {
mRegexps = new HashMap<String, List<Pattern>>();
}
Pattern pattern = Pattern.compile(regexp);
for (String id : ids) {
List<Pattern> paths = mRegexps.get(id);
if (paths == null) {
paths = new ArrayList<Pattern>(n / 2 + 1);
mRegexps.put(id, paths);
}
paths.add(pattern);
}
} catch (PatternSyntaxException e) {
if (!silent) {
formatError("Invalid pattern %1$s under %2$s: %3$s",
regexp, idList, e.getDescription());
}
}
}
private void writeConfig() {
try {
// Write the contents to a new file first such that we don't clobber the
// existing file if some I/O error occurs.
File file = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + ".new"); //$NON-NLS-1$
Writer writer = new BufferedWriter(new FileWriter(file));
writer.write(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$
"<lint>\n"); //$NON-NLS-1$
if (!mSuppressed.isEmpty() || !mSeverity.isEmpty()) {
// Process the maps in a stable sorted order such that if the
// files are checked into version control with the project,
// there are no random diffs just because hashing algorithms
// differ:
Set<String> idSet = new HashSet<String>();
for (String id : mSuppressed.keySet()) {
idSet.add(id);
}
for (String id : mSeverity.keySet()) {
idSet.add(id);
}
List<String> ids = new ArrayList<String>(idSet);
Collections.sort(ids);
for (String id : ids) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writeAttribute(writer, ATTR_ID, id);
Severity severity = mSeverity.get(id);
if (severity != null) {
writeAttribute(writer, ATTR_SEVERITY,
severity.name().toLowerCase(Locale.US));
}
List<Pattern> regexps = mRegexps != null ? mRegexps.get(id) : null;
List<String> paths = mSuppressed.get(id);
if (paths != null && !paths.isEmpty()
|| regexps != null && !regexps.isEmpty()) {
writer.write('>');
writer.write('\n');
// The paths are already kept in sorted order when they are modified
// by ignore(...)
if (paths != null) {
for (String path : paths) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_PATH, path.replace('\\', '/'));
writer.write(" />\n"); //$NON-NLS-1$
}
}
if (regexps != null) {
for (Pattern regexp : regexps) {
writer.write(" <"); //$NON-NLS-1$
writer.write(TAG_IGNORE);
writeAttribute(writer, ATTR_REGEXP, regexp.pattern());
writer.write(" />\n"); //$NON-NLS-1$
}
}
writer.write(" </"); //$NON-NLS-1$
writer.write(TAG_ISSUE);
writer.write('>');
writer.write('\n');
} else {
writer.write(" />\n"); //$NON-NLS-1$
}
}
}
writer.write("</lint>"); //$NON-NLS-1$
writer.close();
// Move file into place: move current version to lint.xml~ (removing the old ~ file
// if it exists), then move the new version to lint.xml.
File oldFile = new File(mConfigFile.getParentFile(),
mConfigFile.getName() + '~'); //$NON-NLS-1$
if (oldFile.exists()) {
oldFile.delete();
}
if (mConfigFile.exists()) {
mConfigFile.renameTo(oldFile);
}
boolean ok = file.renameTo(mConfigFile);
if (ok && oldFile.exists()) {
oldFile.delete();
}
} catch (Exception e) {
mClient.log(e, null);
}
}
private static void writeAttribute(
@NonNull Writer writer, @NonNull String name, @NonNull String value)
throws IOException {
writer.write(' ');
writer.write(name);
writer.write('=');
writer.write('"');
writer.write(value);
writer.write('"');
}
@Override
public void ignore(
@NonNull Context context,
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// This configuration only supports suppressing warnings on a per-file basis
if (location != null) {
ignore(issue, location.getFile());
}
}
/**
* Marks the given issue and file combination as being ignored.
*
* @param issue the issue to be ignored in the given file
* @param file the file to ignore the issue in
*/
public void ignore(@NonNull Issue issue, @NonNull File file) {
ensureInitialized();
String path = mProject != null ? mProject.getRelativePath(file) : file.getPath();
List<String> paths = mSuppressed.get(issue.getId());
if (paths == null) {
paths = new ArrayList<String>();
mSuppressed.put(issue.getId(), paths);
}
paths.add(path);
// Keep paths sorted alphabetically; makes XML output stable
Collections.sort(paths);
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) {
ensureInitialized();
String id = issue.getId();
if (severity == null) {
mSeverity.remove(id);
} else {
mSeverity.put(id, severity);
}
if (!mBulkEditing) {
writeConfig();
}
}
@Override
public void startBulkEditing() {
mBulkEditing = true;
}
@Override
public void finishBulkEditing() {
mBulkEditing = false;
writeConfig();
}
@VisibleForTesting
File getConfigFile() {
return mConfigFile;
}
}
@@ -0,0 +1,288 @@
/*
* 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.client.api;
import static com.android.SdkConstants.ABSOLUTE_LAYOUT;
import static com.android.SdkConstants.ABS_LIST_VIEW;
import static com.android.SdkConstants.ABS_SEEK_BAR;
import static com.android.SdkConstants.ABS_SPINNER;
import static com.android.SdkConstants.ADAPTER_VIEW;
import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKABLE;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.COMPOUND_BUTTON;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW;
import static com.android.SdkConstants.FRAME_LAYOUT;
import static com.android.SdkConstants.GALLERY;
import static com.android.SdkConstants.GRID_VIEW;
import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW;
import static com.android.SdkConstants.IMAGE_BUTTON;
import static com.android.SdkConstants.IMAGE_VIEW;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.LIST_VIEW;
import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW;
import static com.android.SdkConstants.PROGRESS_BAR;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.RADIO_GROUP;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.SCROLL_VIEW;
import static com.android.SdkConstants.SEEK_BAR;
import static com.android.SdkConstants.SPINNER;
import static com.android.SdkConstants.SURFACE_VIEW;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TABLE_LAYOUT;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAB_HOST;
import static com.android.SdkConstants.TAB_WIDGET;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VIEW;
import static com.android.SdkConstants.VIEW_ANIMATOR;
import static com.android.SdkConstants.VIEW_GROUP;
import static com.android.SdkConstants.VIEW_PKG_PREFIX;
import static com.android.SdkConstants.VIEW_STUB;
import static com.android.SdkConstants.VIEW_SWITCHER;
import static com.android.SdkConstants.WEB_VIEW;
import static com.android.SdkConstants.WIDGET_PKG_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Default simple implementation of an {@link SdkInfo}
* <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
class DefaultSdkInfo extends SdkInfo {
@Override
@Nullable
public String getParentViewName(@NonNull String name) {
name = getRawType(name);
return PARENTS.get(name);
}
@Override
@Nullable
public String getParentViewClass(@NonNull String fqcn) {
int index = fqcn.lastIndexOf('.');
if (index != -1) {
fqcn = fqcn.substring(index + 1);
}
String parent = PARENTS.get(fqcn);
if (parent == null) {
return null;
}
// The map only stores class names internally; correct for full package paths
if (parent.equals(VIEW) || parent.equals(VIEW_GROUP) || parent.equals(SURFACE_VIEW)) {
return VIEW_PKG_PREFIX + parent;
} else {
return WIDGET_PKG_PREFIX + parent;
}
}
@Override
public boolean isSubViewOf(@NonNull String parentType, @NonNull String childType) {
String parent = getRawType(parentType);
String child = getRawType(childType);
// Do analysis just on non-fqcn paths
if (parent.indexOf('.') != -1) {
parent = parent.substring(parent.lastIndexOf('.') + 1);
}
if (child.indexOf('.') != -1) {
child = child.substring(child.lastIndexOf('.') + 1);
}
if (parent.equals(VIEW)) {
return true;
}
while (!child.equals(VIEW)) {
if (parent.equals(child)) {
return true;
}
if (implementsInterface(child, parentType)) {
return true;
}
child = PARENTS.get(child);
if (child == null) {
// Unknown view - err on the side of caution
return true;
}
}
return false;
}
private static boolean implementsInterface(String className, String interfaceName) {
return interfaceName.equals(INTERFACES.get(className));
}
// Strip off type parameters, e.g. AdapterView<?> => AdapterView
private static String getRawType(String type) {
if (type != null) {
int index = type.indexOf('<');
if (index != -1) {
type = type.substring(0, index);
}
}
return type;
}
@Override
public boolean isLayout(@NonNull String tag) {
// TODO: Read in widgets.txt from the platform install area to look up this information
// dynamically instead!
if (super.isLayout(tag)) {
return true;
}
return LAYOUTS.contains(tag);
}
private static final int CLASS_COUNT = 59;
private static final int LAYOUT_COUNT = 20;
private static final Map<String,String> PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT);
private static final Set<String> LAYOUTS = Sets.newHashSetWithExpectedSize(CLASS_COUNT);
static {
PARENTS.put(COMPOUND_BUTTON, BUTTON);
PARENTS.put(ABS_SPINNER, ADAPTER_VIEW);
PARENTS.put(ABS_LIST_VIEW, ADAPTER_VIEW);
PARENTS.put(ABS_SEEK_BAR, ADAPTER_VIEW);
PARENTS.put(ADAPTER_VIEW, VIEW_GROUP);
PARENTS.put(VIEW_GROUP, VIEW);
PARENTS.put(TEXT_VIEW, VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put(RADIO_BUTTON, COMPOUND_BUTTON);
PARENTS.put(SPINNER, ABS_SPINNER);
PARENTS.put(IMAGE_BUTTON, IMAGE_VIEW);
PARENTS.put(IMAGE_VIEW, VIEW);
PARENTS.put(EDIT_TEXT, TEXT_VIEW);
PARENTS.put(PROGRESS_BAR, VIEW);
PARENTS.put(TOGGLE_BUTTON, COMPOUND_BUTTON);
PARENTS.put(VIEW_STUB, VIEW);
PARENTS.put(BUTTON, TEXT_VIEW);
PARENTS.put(SEEK_BAR, ABS_SEEK_BAR);
PARENTS.put(CHECK_BOX, COMPOUND_BUTTON);
PARENTS.put(SWITCH, COMPOUND_BUTTON);
PARENTS.put(GALLERY, ABS_SPINNER);
PARENTS.put(SURFACE_VIEW, VIEW);
PARENTS.put(ABSOLUTE_LAYOUT, VIEW_GROUP);
PARENTS.put(LINEAR_LAYOUT, VIEW_GROUP);
PARENTS.put(RELATIVE_LAYOUT, VIEW_GROUP);
PARENTS.put(LIST_VIEW, ABS_LIST_VIEW);
PARENTS.put(VIEW_SWITCHER, VIEW_ANIMATOR);
PARENTS.put(FRAME_LAYOUT, VIEW_GROUP);
PARENTS.put(HORIZONTAL_SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(VIEW_ANIMATOR, FRAME_LAYOUT);
PARENTS.put(TAB_HOST, FRAME_LAYOUT);
PARENTS.put(TABLE_ROW, LINEAR_LAYOUT);
PARENTS.put(RADIO_GROUP, LINEAR_LAYOUT);
PARENTS.put(TAB_WIDGET, LINEAR_LAYOUT);
PARENTS.put(EXPANDABLE_LIST_VIEW, LIST_VIEW);
PARENTS.put(TABLE_LAYOUT, LINEAR_LAYOUT);
PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT);
PARENTS.put(GRID_VIEW, ABS_LIST_VIEW);
PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT);
PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT);
PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW);
PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW);
PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("SlidingDrawer", VIEW_GROUP); //$NON-NLS-1$
PARENTS.put("DialerFilter", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("DigitalClock", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("Chronometer", TEXT_VIEW); //$NON-NLS-1$
PARENTS.put("ImageSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("TextSwitcher", VIEW_SWITCHER); //$NON-NLS-1$
PARENTS.put("AnalogClock", VIEW); //$NON-NLS-1$
PARENTS.put("TwoLineListItem", RELATIVE_LAYOUT); //$NON-NLS-1$
PARENTS.put("ZoomControls", LINEAR_LAYOUT); //$NON-NLS-1$
PARENTS.put("DatePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$
PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$
PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$
PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$
PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$
PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$
assert PARENTS.size() <= CLASS_COUNT : PARENTS.size();
/*
// Check that all widgets lead to the root view
if (LintUtils.assertionsEnabled()) {
for (String key : PARENTS.keySet()) {
String parent = PARENTS.get(key);
if (!parent.equals(VIEW)) {
String grandParent = PARENTS.get(parent);
assert grandParent != null : parent;
}
}
}
*/
LAYOUTS.add(TAB_HOST);
LAYOUTS.add(HORIZONTAL_SCROLL_VIEW);
LAYOUTS.add(VIEW_SWITCHER);
LAYOUTS.add(TAB_WIDGET);
LAYOUTS.add(VIEW_ANIMATOR);
LAYOUTS.add(SCROLL_VIEW);
LAYOUTS.add(GRID_VIEW);
LAYOUTS.add(TABLE_ROW);
LAYOUTS.add(RADIO_GROUP);
LAYOUTS.add(LIST_VIEW);
LAYOUTS.add(EXPANDABLE_LIST_VIEW);
LAYOUTS.add("MediaController"); //$NON-NLS-1$
LAYOUTS.add("DialerFilter"); //$NON-NLS-1$
LAYOUTS.add("ViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("SlidingDrawer"); //$NON-NLS-1$
LAYOUTS.add("StackView"); //$NON-NLS-1$
LAYOUTS.add("SearchView"); //$NON-NLS-1$
LAYOUTS.add("TextSwitcher"); //$NON-NLS-1$
LAYOUTS.add("AdapterViewFlipper"); //$NON-NLS-1$
LAYOUTS.add("ImageSwitcher"); //$NON-NLS-1$
assert LAYOUTS.size() <= LAYOUT_COUNT : LAYOUTS.size();
}
// Currently using a map; this should really be a list, but using a map until we actually
// start adding more than one item
@NonNull
private static final Map<String, String> INTERFACES = new HashMap<String, String>(2);
static {
INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE);
INTERFACES.put(COMPOUND_BUTTON, CHECKABLE);
}
}
@@ -0,0 +1,322 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.annotations.Beta;
import com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Registry which provides a list of checks to be performed on an Android project
* <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 abstract class IssueRegistry {
private static List<Category> sCategories;
private static Map<String, Issue> sIdToIssue;
private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap();
/**
* Creates a new {@linkplain IssueRegistry}
*/
protected IssueRegistry() {
}
private static final Implementation DUMMY_IMPLEMENTATION = new Implementation(Detector.class,
EnumSet.noneOf(Scope.class));
/**
* Issue reported by lint (not a specific detector) when it cannot even
* parse an XML file prior to analysis
*/
@NonNull
public static final Issue PARSER_ERROR = Issue.create(
"ParserError", //$NON-NLS-1$
"Parser Errors",
"Lint will ignore any files that contain fatal parsing errors. These may contain " +
"other errors, or contain code which affects issues in other files.",
Category.CORRECTNESS,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported by lint for various other issues which prevents lint from
* running normally when it's not necessarily an error in the user's code base.
*/
@NonNull
public static final Issue LINT_ERROR = Issue.create(
"LintError", //$NON-NLS-1$
"Lint Failure",
"This issue type represents a problem running lint itself. Examples include " +
"failure to find bytecode for source files (which means certain detectors " +
"could not be run), parsing errors in lint configuration files, etc." +
"\n" +
"These errors are not errors in your own code, but they are shown to make " +
"it clear that some checks were not completed.",
Category.LINT,
10,
Severity.ERROR,
DUMMY_IMPLEMENTATION);
/**
* Issue reported when lint is canceled
*/
@NonNull
public static final Issue CANCELLED = Issue.create(
"LintCanceled", //$NON-NLS-1$
"Lint Canceled",
"Lint canceled by user; the issue report may not be complete.",
Category.LINT,
0,
Severity.INFORMATIONAL,
DUMMY_IMPLEMENTATION);
/**
* Returns the list of issues that can be found by all known detectors.
*
* @return the list of issues to be checked (including those that may be
* disabled!)
*/
@NonNull
public abstract List<Issue> getIssues();
/**
* Get an approximate issue count for a given scope. This is just an optimization,
* so the number does not have to be accurate.
*
* @param scope the scope set
* @return an approximate ceiling of the number of issues expected for a given scope set
*/
protected int getIssueCapacity(@NonNull EnumSet<Scope> scope) {
return 20;
}
/**
* Returns all available issues of a given scope (regardless of whether
* they are actually enabled for a given configuration etc)
*
* @param scope the applicable scope set
* @return a list of issues
*/
@NonNull
protected List<Issue> getIssuesForScope(@NonNull EnumSet<Scope> scope) {
List<Issue> list = sScopeIssues.get(scope);
if (list == null) {
List<Issue> issues = getIssues();
if (scope.equals(Scope.ALL)) {
list = issues;
} else {
list = new ArrayList<Issue>(getIssueCapacity(scope));
for (Issue issue : issues) {
// Determine if the scope matches
if (issue.getImplementation().isAdequate(scope)) {
list.add(issue);
}
}
}
sScopeIssues.put(scope, list);
}
return list;
}
/**
* Creates a list of detectors applicable to the given scope, and with the
* given configuration.
*
* @param client the client to report errors to
* @param configuration the configuration to look up which issues are
* enabled etc from
* @param scope the scope for the analysis, to filter out detectors that
* require wider analysis than is currently being performed
* @param scopeToDetectors an optional map which (if not null) will be
* filled by this method to contain mappings from each scope to
* the applicable detectors for that scope
* @return a list of new detector instances
*/
@NonNull
final List<? extends Detector> createDetectors(
@NonNull LintClient client,
@NonNull Configuration configuration,
@NonNull EnumSet<Scope> scope,
@Nullable Map<Scope, List<Detector>> scopeToDetectors) {
List<Issue> issues = getIssuesForScope(scope);
if (issues.isEmpty()) {
return Collections.emptyList();
}
Set<Class<? extends Detector>> detectorClasses = new HashSet<Class<? extends Detector>>();
Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope =
new HashMap<Class<? extends Detector>, EnumSet<Scope>>();
for (Issue issue : issues) {
Implementation implementation = issue.getImplementation();
Class<? extends Detector> detectorClass = implementation.getDetectorClass();
EnumSet<Scope> issueScope = implementation.getScope();
if (!detectorClasses.contains(detectorClass)) {
// Determine if the issue is enabled
if (!configuration.isEnabled(issue)) {
continue;
}
assert implementation.isAdequate(scope); // Ensured by getIssuesForScope above
detectorClass = client.replaceDetector(detectorClass);
assert detectorClass != null : issue.getId();
detectorClasses.add(detectorClass);
}
if (scopeToDetectors != null) {
EnumSet<Scope> s = detectorToScope.get(detectorClass);
if (s == null) {
detectorToScope.put(detectorClass, issueScope);
} else if (!s.containsAll(issueScope)) {
EnumSet<Scope> union = EnumSet.copyOf(s);
union.addAll(issueScope);
detectorToScope.put(detectorClass, union);
}
}
}
List<Detector> detectors = new ArrayList<Detector>(detectorClasses.size());
for (Class<? extends Detector> clz : detectorClasses) {
try {
Detector detector = clz.newInstance();
detectors.add(detector);
if (scopeToDetectors != null) {
EnumSet<Scope> union = detectorToScope.get(clz);
for (Scope s : union) {
List<Detector> list = scopeToDetectors.get(s);
if (list == null) {
list = new ArrayList<Detector>();
scopeToDetectors.put(s, list);
}
list.add(detector);
}
}
} catch (Throwable t) {
client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$
}
}
return detectors;
}
/**
* Returns true if the given id represents a valid issue id
*
* @param id the id to be checked
* @return true if the given id is valid
*/
public final boolean isIssueId(@NonNull String id) {
return getIssue(id) != null;
}
/**
* Returns true if the given category is a valid category
*
* @param name the category name to be checked
* @return true if the given string is a valid category
*/
public final boolean isCategoryName(@NonNull String name) {
for (Category category : getCategories()) {
if (category.getName().equals(name) || category.getFullName().equals(name)) {
return true;
}
}
return false;
}
/**
* Returns the available categories
*
* @return an iterator for all the categories, never null
*/
@NonNull
public List<Category> getCategories() {
if (sCategories == null) {
final Set<Category> categories = new HashSet<Category>();
for (Issue issue : getIssues()) {
categories.add(issue.getCategory());
}
List<Category> sorted = new ArrayList<Category>(categories);
Collections.sort(sorted);
sCategories = Collections.unmodifiableList(sorted);
}
return sCategories;
}
/**
* Returns the issue for the given id, or null if it's not a valid id
*
* @param id the id to be checked
* @return the corresponding issue, or null
*/
@Nullable
public final Issue getIssue(@NonNull String id) {
if (sIdToIssue == null) {
List<Issue> issues = getIssues();
sIdToIssue = new HashMap<String, Issue>(issues.size());
for (Issue issue : issues) {
sIdToIssue.put(issue.getId(), issue);
}
sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR);
sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR);
}
return sIdToIssue.get(id);
}
/**
* Reset the registry such that it recomputes its available issues.
* <p>
* NOTE: This is only intended for testing purposes.
*/
@VisibleForTesting
protected static void reset() {
sIdToIssue = null;
sCategories = null;
sScopeIssues = Maps.newHashMap();
}
}
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2013 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.client.api;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Severity;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
* <p> An {@link IssueRegistry} for a custom lint rule jar file. The rule jar should provide a
* manifest entry with the key {@code Lint-Registry} and the value of the fully qualified name of an
* implementation of {@link IssueRegistry} (with a default constructor). </p>
*
* <p> NOTE: The custom issue registry should not extend this file; it should be a plain
* IssueRegistry! This file is used internally to wrap the given issue registry.</p>
*/
class JarFileIssueRegistry extends IssueRegistry {
/**
* Manifest constant for declaring an issue provider. Example: Lint-Registry:
* foo.bar.CustomIssueRegistry
*/
private static final String MF_LINT_REGISTRY = "Lint-Registry"; //$NON-NLS-1$
private static Map<File, SoftReference<JarFileIssueRegistry>> sCache;
private final List<Issue> myIssues;
@NonNull
static IssueRegistry get(@NonNull LintClient client, @NonNull File jarFile) throws IOException,
ClassNotFoundException, IllegalAccessException, InstantiationException {
if (sCache == null) {
sCache = new HashMap<File, SoftReference<JarFileIssueRegistry>>();
} else {
SoftReference<JarFileIssueRegistry> reference = sCache.get(jarFile);
if (reference != null) {
JarFileIssueRegistry registry = reference.get();
if (registry != null) {
return registry;
}
}
}
JarFileIssueRegistry registry = new JarFileIssueRegistry(client, jarFile);
sCache.put(jarFile, new SoftReference<JarFileIssueRegistry>(registry));
return registry;
}
private JarFileIssueRegistry(@NonNull LintClient client, @NonNull File file)
throws IOException, ClassNotFoundException, IllegalAccessException,
InstantiationException {
myIssues = Lists.newArrayList();
JarFile jarFile = null;
try {
jarFile = new JarFile(file);
Manifest manifest = jarFile.getManifest();
Attributes attrs = manifest.getMainAttributes();
Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY));
if (object instanceof String) {
String className = (String) object;
// Make a class loader for this jar
URL url = SdkUtils.fileToUrl(file);
URLClassLoader loader = new URLClassLoader(new URL[]{url},
JarFileIssueRegistry.class.getClassLoader());
Class<?> registryClass = Class.forName(className, true, loader);
IssueRegistry registry = (IssueRegistry) registryClass.newInstance();
myIssues.addAll(registry.getIssues());
} else {
client.log(Severity.ERROR, null,
"Custom lint rule jar %1$s does not contain a valid registry manifest key " +
"(%2$s).\n" +
"Either the custom jar is invalid, or it uses an outdated API not supported " +
"this lint client", file.getPath(), MF_LINT_REGISTRY);
}
} finally {
if (jarFile != null) {
jarFile.close();
}
}
}
@NonNull
@Override
public List<Issue> getIssues() {
return myIssues;
}
}
@@ -0,0 +1,530 @@
/*
* 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.client.api;
import static com.android.SdkConstants.ATTR_VALUE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import java.util.Collections;
import java.util.List;
import lombok.ast.Identifier;
import lombok.ast.Node;
import lombok.ast.StrictListAccessor;
import lombok.ast.TypeReference;
import lombok.ast.TypeReferencePart;
/**
* A wrapper for a Java parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in Java editors.
* <p/>
* <b>NOTE: This is not public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
public abstract class JavaParser {
public static final String TYPE_OBJECT = "java.lang.Object"; //$NON-NLS-1$
public static final String TYPE_STRING = "java.lang.String"; //$NON-NLS-1$
public static final String TYPE_INT = "int"; //$NON-NLS-1$
public static final String TYPE_LONG = "long"; //$NON-NLS-1$
public static final String TYPE_CHAR = "char"; //$NON-NLS-1$
public static final String TYPE_FLOAT = "float"; //$NON-NLS-1$
public static final String TYPE_DOUBLE = "double"; //$NON-NLS-1$
public static final String TYPE_BOOLEAN = "boolean"; //$NON-NLS-1$
public static final String TYPE_SHORT = "short"; //$NON-NLS-1$
public static final String TYPE_BYTE = "byte"; //$NON-NLS-1$
public static final String TYPE_NULL = "null"; //$NON-NLS-1$
/**
* Prepare to parse the given contexts. This method will be called before
* a series of {@link #parseJava(JavaContext)} calls, which allows some
* parsers to do up front global computation in case they want to more
* efficiently process multiple files at the same time. This allows a single
* type-attribution pass for example, which is a lot more efficient than
* performing global type analysis over and over again for each individual
* file
*
* @param contexts a list of contexts to be parsed
*/
public abstract void prepareJavaParse(@NonNull List<JavaContext> contexts);
/**
* Parse the file pointed to by the given context.
*
* @param context the context pointing to the file to be parsed, typically
* via {@link Context#getContents()} but the file handle (
* {@link Context#file} can also be used to map to an existing
* editor buffer in the surrounding tool, etc)
* @return the compilation unit node for the file
*/
@Nullable
public abstract Node parseJava(@NonNull JavaContext context);
/**
* Returns a {@link Location} for the given node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull JavaContext context, @NonNull Node node);
/**
* Creates a light-weight handle to a location for the given node. It can be
* turned into a full fledged location by
* {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}.
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return a location handle
*/
@NonNull
public abstract Location.Handle createLocationHandle(@NonNull JavaContext context,
@NonNull Node node);
/**
* Dispose any data structures held for the given context.
* @param context information about the file previously parsed
* @param compilationUnit the compilation unit being disposed
*/
public void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit) {
}
/**
* Dispose any remaining data structures held for all contexts.
* Typically frees up any resources allocated by
* {@link #prepareJavaParse(List)}
*/
public void dispose() {
}
/**
* Resolves the given expression node: computes the declaration for the given symbol
*
* @param context information about the file being parsed
* @param node the node to resolve
* @return a node representing the resolved fully type: class/interface/annotation,
* field, method or variable
*/
@Nullable
public abstract ResolvedNode resolve(@NonNull JavaContext context, @NonNull Node node);
/**
* Finds the given type, if possible (which should be reachable from the compilation
* patch of the given node.
*
* @param context information about the file being parsed
* @param fullyQualifiedName the fully qualified name of the class to look up
* @return the class, or null if not found
*/
@Nullable
public ResolvedClass findClass(
@NonNull JavaContext context,
@NonNull String fullyQualifiedName) {
return null;
}
/**
* Gets the type of the given node
*
* @param context information about the file being parsed
* @param node the node to look up the type for
* @return the type of the node, if known
*/
@Nullable
public abstract TypeDescriptor getType(@NonNull JavaContext context, @NonNull Node node);
/** A description of a type, such as a primitive int or the android.app.Activity class */
public abstract static class TypeDescriptor {
/**
* Returns the fully qualified name of the type, such as "int" or "android.app.Activity"
* */
@NonNull public abstract String getName();
/**
* Returns the full signature of the type, which is normally the same as {@link #getName()}
* but for arrays can include []'s, for generic methods can include generics parameters
* etc
*/
@NonNull public abstract String getSignature();
public abstract boolean matchesName(@NonNull String name);
public abstract boolean matchesSignature(@NonNull String signature);
@NonNull
public TypeReference getNode() {
TypeReference typeReference = new TypeReference();
StrictListAccessor<TypeReferencePart, TypeReference> parts = typeReference.astParts();
for (String part : Splitter.on('.').split(getName())) {
Identifier identifier = Identifier.of(part);
parts.addToEnd(new TypeReferencePart().astIdentifier(identifier));
}
return typeReference;
}
/** If the type is not primitive, returns the class of the type if known */
@Nullable
public abstract ResolvedClass getTypeClass();
@Override
public abstract boolean equals(Object o);
}
/** Convenience implementation of {@link TypeDescriptor} */
public static class DefaultTypeDescriptor extends TypeDescriptor {
private String mName;
public DefaultTypeDescriptor(String name) {
mName = name;
}
@NonNull
@Override
public String getName() {
return mName;
}
@NonNull
@Override
public String getSignature() {
return getName();
}
@Override
public boolean matchesName(@NonNull String name) {
return mName.equals(name);
}
@Override
public boolean matchesSignature(@NonNull String signature) {
return matchesName(signature);
}
@Override
public String toString() {
return getSignature();
}
@Override
@Nullable
public ResolvedClass getTypeClass() {
return null;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultTypeDescriptor that = (DefaultTypeDescriptor) o;
return !(mName != null ? !mName.equals(that.mName) : that.mName != null);
}
@Override
public int hashCode() {
return mName != null ? mName.hashCode() : 0;
}
}
/** A resolved declaration from an AST Node reference */
public abstract static class ResolvedNode {
@NonNull
public abstract String getName();
/** Returns the signature of the resolved node */
public abstract String getSignature();
public abstract int getModifiers();
@Override
public String toString() {
return getSignature();
}
/** Returns any annotations defined on this node */
@NonNull
public abstract Iterable<ResolvedAnnotation> getAnnotations();
/**
* Searches for the annotation of the given type on this node
*
* @param type the fully qualified name of the annotation to check
* @return the annotation, or null if not found
*/
@Nullable
public ResolvedAnnotation getAnnotation(@NonNull String type) {
for (ResolvedAnnotation annotation : getAnnotations()) {
if (annotation.getType().matchesSignature(type)) {
return annotation;
}
}
return null;
}
}
/** A resolved class declaration (class, interface, enumeration or annotation) */
public abstract static class ResolvedClass extends ResolvedNode {
/** Returns the fully qualified name of this class */
@Override
@NonNull
public abstract String getName();
/** Returns the simple of this class */
@NonNull
public abstract String getSimpleName();
/** Returns the package name of this class */
@NonNull
public String getPackageName() {
String name = getName();
String simpleName = getSimpleName();
if (name.length() > simpleName.length() + 1) {
return name.substring(0, name.length() - simpleName.length() - 1);
}
return name;
}
/** Returns whether this class' fully qualified name matches the given name */
public abstract boolean matches(@NonNull String name);
@Nullable
public abstract ResolvedClass getSuperClass();
@Nullable
public abstract ResolvedClass getContainingClass();
public TypeDescriptor getType() {
return new DefaultTypeDescriptor(getName());
}
/**
* Determines whether this class extends the given name. If strict is true,
* it will not consider C extends C true.
*
* @param name the fully qualified class name
* @param strict if true, do not consider a class to be extending itself
* @return true if this class extends the given class
*/
public abstract boolean isSubclassOf(@NonNull String name, boolean strict);
@NonNull
public abstract Iterable<ResolvedMethod> getConstructors();
/** Returns the methods defined in this class, and optionally any methods inherited from any superclasses as well */
@NonNull
public abstract Iterable<ResolvedMethod> getMethods(boolean includeInherited);
/** Returns the methods of a given name defined in this class, and optionally any methods inherited from any superclasses as well */
@NonNull
public abstract Iterable<ResolvedMethod> getMethods(@NonNull String name, boolean includeInherited);
/** Returns the named field defined in this class, or optionally inherited from a superclass */
@Nullable
public abstract ResolvedField getField(@NonNull String name, boolean includeInherited);
}
/** A method or constructor declaration */
public abstract static class ResolvedMethod extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this method name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract ResolvedClass getContainingClass();
public abstract int getArgumentCount();
@NonNull
public abstract TypeDescriptor getArgumentType(int index);
/** Returns true if the parameter at the given index matches the given type signature */
public boolean argumentMatchesType(int index, @NonNull String signature) {
return getArgumentType(index).matchesSignature(signature);
}
@Nullable
public abstract TypeDescriptor getReturnType();
public boolean isConstructor() {
return getReturnType() == null;
}
/** Returns any annotations defined on the given parameter of this method */
@NonNull
public abstract Iterable<ResolvedAnnotation> getParameterAnnotations(int index);
/**
* Searches for the annotation of the given type on the method
*
* @param type the fully qualified name of the annotation to check
* @param parameterIndex the index of the parameter to look up
* @return the annotation, or null if not found
*/
@Nullable
public ResolvedAnnotation getParameterAnnotation(@NonNull String type,
int parameterIndex) {
for (ResolvedAnnotation annotation : getParameterAnnotations(parameterIndex)) {
if (annotation.getType().matchesSignature(type)) {
return annotation;
}
}
return null;
}
/** Returns the super implementation of the given method, if any */
@Nullable
public ResolvedMethod getSuperMethod() {
ResolvedClass cls = getContainingClass().getSuperClass();
if (cls != null) {
String methodName = getName();
int argCount = getArgumentCount();
for (ResolvedMethod method : cls.getMethods(methodName, true)) {
if (argCount != method.getArgumentCount()) {
continue;
}
boolean sameTypes = true;
for (int arg = 0; arg < argCount; arg++) {
if (!method.getArgumentType(arg).equals(getArgumentType(arg))) {
sameTypes = false;
break;
}
}
if (sameTypes) {
return method;
}
}
}
return null;
}
}
/** A field declaration */
public abstract static class ResolvedField extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this field name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
@NonNull
public abstract ResolvedClass getContainingClass();
@Nullable
public abstract Object getValue();
public String getContainingClassName() {
return getContainingClass().getName();
}
}
/**
* An annotation <b>reference</b>. Note that this refers to a usage of an annotation,
* not a declaraton of an annotation. You can call {@link #getClassType()} to
* find the declaration for the annotation.
*/
public abstract static class ResolvedAnnotation extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this field name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
/** Returns the {@link ResolvedClass} which defines the annotation */
@Nullable
public abstract ResolvedClass getClassType();
public static class Value {
@NonNull public final String name;
@Nullable public final Object value;
public Value(@NonNull String name, @Nullable Object value) {
this.name = name;
this.value = value;
}
}
@NonNull
public abstract List<Value> getValues();
@Nullable
public Object getValue(@NonNull String name) {
for (Value value : getValues()) {
if (name.equals(value.name)) {
return value.value;
}
}
return null;
}
@Nullable
public Object getValue() {
return getValue(ATTR_VALUE);
}
@NonNull
@Override
public Iterable<ResolvedAnnotation> getAnnotations() {
return Collections.emptyList();
}
}
/** A local variable or parameter declaration */
public abstract static class ResolvedVariable extends ResolvedNode {
@Override
@NonNull
public abstract String getName();
/** Returns whether this variable name matches the given name */
public abstract boolean matches(@NonNull String name);
@NonNull
public abstract TypeDescriptor getType();
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.google.common.annotations.Beta;
/**
* Interface implemented by listeners to be notified of lint events
* <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 interface LintListener {
/** The various types of events provided to lint listeners */
enum EventType {
/** A lint check is about to begin */
STARTING,
/** Lint is about to check the given project, see {@link Context#getProject()} */
SCANNING_PROJECT,
/** Lint is about to check the given library project, see {@link Context#getProject()} */
SCANNING_LIBRARY_PROJECT,
/** Lint is about to check the given file, see {@link Context#file} */
SCANNING_FILE,
/** A new pass was initiated */
NEW_PHASE,
/** The lint check was canceled */
CANCELED,
/** The lint check is done */
COMPLETED,
}
/**
* Notifies listeners that the event of the given type has occurred.
* Additional information, such as the file being scanned, or the project
* being scanned, is available in the {@link Context} object (except for the
* {@link EventType#STARTING}, {@link EventType#CANCELED} or
* {@link EventType#COMPLETED} events which are fired outside of project
* contexts.)
*
* @param driver the driver running through the checks
* @param type the type of event that occurred
* @param context the context providing additional information
*/
void update(@NonNull LintDriver driver, @NonNull EventType type,
@Nullable Context context);
}
@@ -0,0 +1,179 @@
/*
* Copyright (C) 2013 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* Information about a request to run lint
* <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 LintRequest {
@NonNull
protected final LintClient mClient;
@NonNull
protected final List<File> mFiles;
@Nullable
protected EnumSet<Scope> mScope;
@Nullable
protected Boolean mReleaseMode;
@Nullable
protected Collection<Project> mProjects;
/**
* Creates a new {@linkplain LintRequest}, to be passed to a {@link LintDriver}
*
* @param client the tool wrapping the analyzer, such as an IDE or a CLI
* @param files the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*/
public LintRequest(@NonNull LintClient client, @NonNull List<File> files) {
mClient = client;
mFiles = files;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mClient;
}
/**
* Returns the set of files to check with lint. This can reference Android projects,
* or directories containing Android projects, or individual XML or Java files
* (typically for incremental IDE analysis).
*
* @return the set of files to check, should not be empty
*/
@NonNull
public List<File> getFiles() {
return mFiles;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @return the scope to use, or null to use the default
*/
@Nullable
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Sets the scope to use; lint checks which require a wider scope set
* will be ignored
*
* @param scope the scope
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setScope(@Nullable EnumSet<Scope> scope) {
mScope = scope;
return this;
}
/**
* Returns {@code true} if lint is invoked as part of a release mode build,
* {@code false} if it is part of a debug mode build, and {@code null} if
* the release mode is not known
*
* @return true if this lint is running in release mode, null if not known
*/
@Nullable
public Boolean isReleaseMode() {
return mReleaseMode;
}
/**
* Sets the release mode. Use {@code true} if lint is invoked as part of a
* release mode build, {@code false} if it is part of a debug mode build,
* and {@code null} if the release mode is not known
*
* @param releaseMode true if this lint is running in release mode, null if not known
* @return this, for constructor chaining
*/
@NonNull
public LintRequest setReleaseMode(@Nullable Boolean releaseMode) {
mReleaseMode = releaseMode;
return this;
}
/**
* Gets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @return a collection of projects, or null
*/
@Nullable
public Collection<Project> getProjects() {
return mProjects;
}
/**
* Sets the projects for the lint requests. This is optional; if not provided lint will search
* the {@link #getFiles()} directories and look for projects via {@link
* LintClient#isProjectDirectory(java.io.File)}. However, this method allows a lint client to
* set up all the projects ahead of time, and associate those projects with native resources
* (in an IDE for example, each lint project can be associated with the corresponding IDE
* project).
*
* @param projects a collection of projects, or null
*/
public void setProjects(@Nullable Collection<Project> projects) {
mProjects = projects;
}
/**
* Returns the project to be used as the main project during analysis. This is
* usually the project itself, but when you are for example analyzing a library project,
* it can be the app project using the library.
*
* @param project the project to look up the main project for
* @return the main project
*/
@NonNull
public Project getMainProject(@NonNull Project project) {
return project;
}
}
@@ -0,0 +1,207 @@
/*
* Copyright (C) 2013 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.client.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FD_ASSETS;
import static com.android.tools.lint.detector.api.Detector.OtherFileScanner;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import java.io.File;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Visitor for "other" files: files that aren't java sources,
* XML sources, etc -- or which should have custom handling in some
* other way.
*/
class OtherFileVisitor {
@NonNull
private final List<Detector> mDetectors;
@NonNull
private Map<Scope, List<File>> mFiles = new EnumMap<Scope, List<File>>(Scope.class);
OtherFileVisitor(@NonNull List<Detector> detectors) {
mDetectors = detectors;
}
/** Analyze other files in the given project */
void scan(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main) {
// Collect all project files
File projectFolder = project.getDir();
EnumSet<Scope> scopes = EnumSet.noneOf(Scope.class);
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> applicable = fileScanner.getApplicableFiles();
if (applicable.contains(Scope.OTHER)) {
scopes = Scope.ALL;
break;
}
scopes.addAll(applicable);
}
List<File> subset = project.getSubset();
if (scopes.contains(Scope.RESOURCE_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (SdkUtils.endsWith(file.getPath(), DOT_XML) &&
!file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File res : project.getResourceFolders()) {
collectFiles(files, res);
}
File assets = new File(projectFolder, FD_ASSETS);
if (assets.exists()) {
collectFiles(files, assets);
}
if (!files.isEmpty()) {
mFiles.put(Scope.RESOURCE_FILE, files);
}
}
}
if (scopes.contains(Scope.JAVA_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_JAVA)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File srcFolder : project.getJavaSourceFolders()) {
collectFiles(files, srcFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.JAVA_FILE, files);
}
}
}
if (scopes.contains(Scope.CLASS_FILE)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getPath().endsWith(DOT_CLASS)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
} else {
List<File> files = Lists.newArrayListWithExpectedSize(100);
for (File classFolder : project.getJavaClassFolders()) {
collectFiles(files, classFolder);
}
if (!files.isEmpty()) {
mFiles.put(Scope.CLASS_FILE, files);
}
}
}
if (scopes.contains(Scope.MANIFEST)) {
if (subset != null && !subset.isEmpty()) {
List<File> files = new ArrayList<File>(subset.size());
for (File file : subset) {
if (file.getName().equals(ANDROID_MANIFEST_XML)) {
files.add(file);
}
}
if (!files.isEmpty()) {
mFiles.put(Scope.MANIFEST, files);
}
} else {
List<File> manifestFiles = project.getManifestFiles();
if (manifestFiles != null) {
mFiles.put(Scope.MANIFEST, manifestFiles);
}
}
}
for (Map.Entry<Scope, List<File>> entry : mFiles.entrySet()) {
Scope scope = entry.getKey();
List<File> files = entry.getValue();
List<Detector> applicable = new ArrayList<Detector>(mDetectors.size());
for (Detector detector : mDetectors) {
OtherFileScanner fileScanner = (OtherFileScanner) detector;
EnumSet<Scope> appliesTo = fileScanner.getApplicableFiles();
if (appliesTo.contains(Scope.OTHER) || appliesTo.contains(scope)) {
applicable.add(detector);
}
}
if (!applicable.isEmpty()) {
for (File file : files) {
Context context = new Context(driver, project, main, file);
for (Detector detector : applicable) {
detector.beforeCheckFile(context);
detector.run(context);
detector.afterCheckFile(context);
}
if (driver.isCanceled()) {
return;
}
}
}
}
}
private static void collectFiles(List<File> files, File file) {
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
collectFiles(files, child);
}
}
} else {
files.add(file);
}
}
}
@@ -0,0 +1,245 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.ResourceContext;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.RandomAccess;
/**
* Specialized visitor for running detectors on resources: typically XML documents,
* but also binary resources.
* <p>
* It operates in two phases:
* <ol>
* <li> First, it computes a set of maps where it generates a map from each
* significant element name, and each significant attribute name, to a list
* of detectors to consult for that element or attribute name.
* The set of element names or attribute names (or both) that a detector
* is interested in is provided by the detectors themselves.
* <li> Second, it iterates over the document a single time. For each element and
* attribute it looks up the list of interested detectors, and runs them.
* </ol>
* It also notifies all the detectors before and after the document is processed
* such that they can do pre- and post-processing.
* <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
class ResourceVisitor {
private final Map<String, List<Detector.XmlScanner>> mElementToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final Map<String, List<Detector.XmlScanner>> mAttributeToCheck =
new HashMap<String, List<Detector.XmlScanner>>();
private final List<Detector.XmlScanner> mDocumentDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllElementDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<Detector.XmlScanner> mAllAttributeDetectors =
new ArrayList<Detector.XmlScanner>();
private final List<? extends Detector> mAllDetectors;
private final List<? extends Detector> mBinaryDetectors;
private final XmlParser mParser;
// Really want this:
//<T extends List<Detector> & Detector.XmlScanner> XmlVisitor(IDomParser parser,
// T xmlDetectors) {
// but it makes client code tricky and ugly.
ResourceVisitor(
@NonNull XmlParser parser,
@NonNull List<? extends Detector> xmlDetectors,
@Nullable List<Detector> binaryDetectors) {
mParser = parser;
mAllDetectors = xmlDetectors;
mBinaryDetectors = binaryDetectors;
// TODO: Check appliesTo() for files, and find a quick way to enable/disable
// rules when running through a full project!
for (Detector detector : xmlDetectors) {
Detector.XmlScanner xmlDetector = (XmlScanner) detector;
Collection<String> attributes = xmlDetector.getApplicableAttributes();
if (attributes == XmlScanner.ALL) {
mAllAttributeDetectors.add(xmlDetector);
} else if (attributes != null) {
for (String attribute : attributes) {
List<Detector.XmlScanner> list = mAttributeToCheck.get(attribute);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mAttributeToCheck.put(attribute, list);
}
list.add(xmlDetector);
}
}
Collection<String> elements = xmlDetector.getApplicableElements();
if (elements == XmlScanner.ALL) {
mAllElementDetectors.add(xmlDetector);
} else if (elements != null) {
for (String element : elements) {
List<Detector.XmlScanner> list = mElementToCheck.get(element);
if (list == null) {
list = new ArrayList<Detector.XmlScanner>();
mElementToCheck.put(element, list);
}
list.add(xmlDetector);
}
}
if ((attributes == null || (attributes.isEmpty()
&& attributes != XmlScanner.ALL))
&& (elements == null || (elements.isEmpty()
&& elements != XmlScanner.ALL))) {
mDocumentDetectors.add(xmlDetector);
}
}
}
void visitFile(@NonNull XmlContext context, @NonNull File file) {
assert LintUtils.isXmlFile(file);
try {
if (context.document == null) {
context.document = mParser.parseXml(context);
if (context.document == null) {
// No need to log this; the parser should be reporting
// a full warning (such as IssueRegistry#PARSER_ERROR)
// with details, location, etc.
return;
}
if (context.document.getDocumentElement() == null) {
// Ignore empty documents
return;
}
}
for (Detector check : mAllDetectors) {
check.beforeCheckFile(context);
}
for (Detector.XmlScanner check : mDocumentDetectors) {
check.visitDocument(context, context.document);
}
if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty()
|| !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) {
visitElement(context, context.document.getDocumentElement());
}
for (Detector check : mAllDetectors) {
check.afterCheckFile(context);
}
} finally {
if (context.document != null) {
mParser.dispose(context, context.document);
context.document = null;
}
}
}
private void visitElement(@NonNull XmlContext context, @NonNull Element element) {
List<Detector.XmlScanner> elementChecks = mElementToCheck.get(element.getTagName());
if (elementChecks != null) {
assert elementChecks instanceof RandomAccess;
for (XmlScanner check : elementChecks) {
check.visitElement(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElement(context, element);
}
}
if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) {
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name == null) {
name = attribute.getName();
}
List<Detector.XmlScanner> list = mAttributeToCheck.get(name);
if (list != null) {
for (XmlScanner check : list) {
check.visitAttribute(context, attribute);
}
}
if (!mAllAttributeDetectors.isEmpty()) {
for (XmlScanner check : mAllAttributeDetectors) {
check.visitAttribute(context, attribute);
}
}
}
}
// Visit children
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
visitElement(context, (Element) child);
}
}
// Post hooks
if (elementChecks != null) {
for (XmlScanner check : elementChecks) {
check.visitElementAfter(context, element);
}
}
if (!mAllElementDetectors.isEmpty()) {
for (XmlScanner check : mAllElementDetectors) {
check.visitElementAfter(context, element);
}
}
}
@NonNull
public XmlParser getParser() {
return mParser;
}
public void visitBinaryResource(@NonNull ResourceContext context) {
if (mBinaryDetectors == null) {
return;
}
for (Detector check : mBinaryDetectors) {
check.beforeCheckFile(context);
check.checkBinaryResource(context);
check.afterCheckFile(context);
}
}
}
@@ -0,0 +1,90 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Information about SDKs
* <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 abstract class SdkInfo {
/**
* Returns true if the given child view is the same class or a sub class of
* the given parent view class
*
* @param parentViewFqcn the fully qualified class name of the parent view
* @param childViewFqcn the fully qualified class name of the child view
* @return true if the child view is a sub view of (or the same class as)
* the parent view
*/
public boolean isSubViewOf(@NonNull String parentViewFqcn, @NonNull String childViewFqcn) {
while (!childViewFqcn.equals("android.view.View")) { //$NON-NLS-1$
if (parentViewFqcn.equals(childViewFqcn)) {
return true;
}
String parent = getParentViewClass(childViewFqcn);
if (parent == null) {
// Unknown view - err on the side of caution
return true;
}
childViewFqcn = parent;
}
return false;
}
/**
* Returns the fully qualified name of the parent view, or null if the view
* is the root android.view.View class.
*
* @param fqcn the fully qualified class name of the view
* @return the fully qualified class name of the parent view, or null
*/
@Nullable
public abstract String getParentViewClass(@NonNull String fqcn);
/**
* Returns the class name of the parent view, or null if the view is the
* root android.view.View class. This is the same as the
* {@link #getParentViewClass(String)} but without the package.
*
* @param name the view class name to look up the parent for (not including
* package)
* @return the view name of the parent
*/
@Nullable
public abstract String getParentViewName(@NonNull String name);
/**
* Returns true if the given widget name is a layout
*
* @param tag the XML tag for the view
* @return true if the given tag corresponds to a layout
*/
public boolean isLayout(@NonNull String tag) {
return tag.endsWith("Layout"); //$NON-NLS-1$
}
// TODO: Add access to resource resolution here.
}
@@ -0,0 +1,137 @@
/*
* 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.client.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
/**
* A wrapper for an XML parser. This allows tools integrating lint to map directly
* to builtin services, such as already-parsed data structures in XML editors.
* <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 abstract class XmlParser {
/**
* Parse the file pointed to by the given context and return as a Document
*
* @param context the context pointing to the file to be parsed, typically
* via {@link Context#getContents()} but the file handle (
* {@link Context#file} can also be used to map to an existing
* editor buffer in the surrounding tool, etc)
* @return the parsed DOM document, or null if parsing fails
*/
@Nullable
public abstract Document parseXml(@NonNull XmlContext context);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node. Like
* {@link #getLocation(XmlContext, Node)}, but allows a position range that
* is a subset of the node range.
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @param start the starting position within the node, inclusive
* @param end the ending position within the node, exclusive
* @return a location for the given node
*/
@NonNull
public abstract Location getLocation(@NonNull XmlContext context, @NonNull Node node,
int start, int end);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getNameLocation(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns a {@link Location} for the given DOM node
*
* @param context information about the file being parsed
* @param node the node to create a location for
* @return a location for the given node
*/
@NonNull
public abstract Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node);
/**
* Creates a light-weight handle to a location for the given node. It can be
* turned into a full fledged location by
* {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}.
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return a location handle
*/
@NonNull
public abstract Location.Handle createLocationHandle(@NonNull XmlContext context,
@NonNull Node node);
/**
* Dispose any data structures held for the given context.
* @param context information about the file previously parsed
* @param document the document that was parsed and is now being disposed
*/
public void dispose(@NonNull XmlContext context, @NonNull Document document) {
}
/**
* Returns the start offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the start offset, or -1 if not known
*/
public abstract int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node);
/**
* Returns the end offset of the given node, or -1 if not known
*
* @param context the context providing the node
* @param node the node (element or attribute) to create a location handle
* for
* @return the end offset, or -1 if not known
*/
public abstract int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node);
}
@@ -0,0 +1,155 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* A category is a container for related issues.
* <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 final class Category implements Comparable<Category> {
private final String mName;
private final int mPriority;
private final Category mParent;
/**
* Creates a new {@link Category}.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
*/
private Category(
@Nullable Category parent,
@NonNull String name,
int priority) {
mParent = parent;
mName = name;
mPriority = priority;
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@NonNull String name, int priority) {
return new Category(null, name, priority);
}
/**
* Creates a new top level {@link Category} with the given sorting priority.
*
* @param parent the name of a parent category, or null
* @param name the name of the category
* @param priority a sorting priority, with higher being more important
* @return a new category
*/
@NonNull
public static Category create(@Nullable Category parent, @NonNull String name, int priority) {
return new Category(parent, name, priority);
}
/**
* Returns the parent category, or null if this is a top level category
*
* @return the parent category, or null if this is a top level category
*/
public Category getParent() {
return mParent;
}
/**
* Returns the name of this category
*
* @return the name of this category
*/
public String getName() {
return mName;
}
/**
* Returns a full name for this category. For a top level category, this is just
* the {@link #getName()} value, but for nested categories it will include the parent
* names as well.
*
* @return a full name for this category
*/
public String getFullName() {
if (mParent != null) {
return mParent.getFullName() + ':' + mName;
} else {
return mName;
}
}
@Override
public int compareTo(Category other) {
if (other.mPriority == mPriority) {
if (mParent == other) {
return 1;
} else if (other.mParent == this) {
return -1;
}
}
return other.mPriority - mPriority;
}
/** Issues related to running lint itself */
public static final Category LINT = create("Lint", 110);
/** Issues related to correctness */
public static final Category CORRECTNESS = create("Correctness", 100);
/** Issues related to security */
public static final Category SECURITY = create("Security", 90);
/** Issues related to performance */
public static final Category PERFORMANCE = create("Performance", 80);
/** Issues related to usability */
public static final Category USABILITY = create("Usability", 70);
/** Issues related to accessibility */
public static final Category A11Y = create("Accessibility", 60);
/** Issues related to internationalization */
public static final Category I18N = create("Internationalization", 50);
/** Issues related to right to left and bi-directional text support */
public static final Category RTL = create("Bi-directional Text", 40);
// Sub categories
/** Issues related to icons */
public static final Category ICONS = create(USABILITY, "Icons", 73);
/** Issues related to typography */
public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76);
/** Issues related to messages/strings */
public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95);
}
@@ -0,0 +1,720 @@
/*
* 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.detector.api;
import static com.android.SdkConstants.CONSTRUCTOR_NAME;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.EOL_BACKWARD;
import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Location.SearchDirection;
import com.android.tools.lint.detector.api.Location.SearchHints;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.LineNumberNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import java.io.File;
import java.util.List;
/**
* A {@link Context} used when checking .class files.
* <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 ClassContext extends Context {
private final File mBinDir;
/** The class file DOM root node */
private final ClassNode mClassNode;
/** The class file byte data */
private final byte[] mBytes;
/** The source file, if known/found */
private File mSourceFile;
/** The contents of the source file, if source file is known/found */
private String mSourceContents;
/** Whether we've searched for the source file (used to avoid repeated failed searches) */
private boolean mSearchedForSource;
/** If the file is a relative path within a jar file, this is the jar file, otherwise null */
private final File mJarFile;
/** Whether this class is part of a library (rather than corresponding to one of the
* source files in this project */
private final boolean mFromLibrary;
/**
* Construct a new {@link ClassContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is the
* root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param jarFile If the file is a relative path within a jar file, this is
* the jar file, otherwise null
* @param binDir the root binary directory containing this .class file.
* @param bytes the bytecode raw data
* @param classNode the bytecode object model
* @param fromLibrary whether this class is from a library rather than part
* of this project
* @param sourceContents initial contents of the Java source, if known, or
* null
*/
public ClassContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes,
@NonNull ClassNode classNode,
boolean fromLibrary,
@Nullable String sourceContents) {
super(driver, project, main, file);
mJarFile = jarFile;
mBinDir = binDir;
mBytes = bytes;
mClassNode = classNode;
mFromLibrary = fromLibrary;
mSourceContents = sourceContents;
}
/**
* Returns the raw bytecode data for this class file
*
* @return the byte array containing the bytecode data
*/
@NonNull
public byte[] getBytecode() {
return mBytes;
}
/**
* Returns the bytecode object model
*
* @return the bytecode object model, never null
*/
@NonNull
public ClassNode getClassNode() {
return mClassNode;
}
/**
* Returns the jar file, if any. If this is null, the .class file is a real file
* on disk, otherwise it represents a relative path within the jar file.
*
* @return the jar file, or null
*/
@Nullable
public File getJarFile() {
return mJarFile;
}
/**
* Returns whether this class is part of a library (not this project).
*
* @return true if this class is part of a library
*/
public boolean isFromClassLibrary() {
return mFromLibrary;
}
/**
* Returns the source file for this class file, if possible.
*
* @return the source file, or null
*/
@Nullable
public File getSourceFile() {
if (mSourceFile == null && !mSearchedForSource) {
mSearchedForSource = true;
String source = mClassNode.sourceFile;
if (source == null) {
source = file.getName();
if (source.endsWith(DOT_CLASS)) {
source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA;
}
int index = source.indexOf('$');
if (index != -1) {
source = source.substring(0, index) + DOT_JAVA;
}
}
if (source != null) {
if (mJarFile != null) {
String relative = file.getParent() + File.separator + source;
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
} else {
// Determine package
String topPath = mBinDir.getPath();
String parentPath = file.getParentFile().getPath();
if (parentPath.startsWith(topPath)) {
int start = topPath.length() + 1;
String relative = start > parentPath.length() ? // default package?
"" : parentPath.substring(start);
List<File> sources = getProject().getJavaSourceFolders();
for (File dir : sources) {
File sourceFile = new File(dir, relative + File.separator + source);
if (sourceFile.exists()) {
mSourceFile = sourceFile;
break;
}
}
}
}
}
}
return mSourceFile;
}
/**
* Returns the contents of the source file for this class file, if found.
*
* @return the source contents, or ""
*/
@NonNull
public String getSourceContents() {
if (mSourceContents == null) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
mSourceContents = getClient().readFile(mSourceFile);
}
if (mSourceContents == null) {
mSourceContents = "";
}
}
return mSourceContents;
}
/**
* Returns the contents of the source file for this class file, if found. If
* {@code read} is false, do not read the source contents if it has not
* already been read. (This is primarily intended for the lint
* infrastructure; most client code would call {@link #getSourceContents()}
* .)
*
* @param read whether to read the source contents if it has not already
* been initialized
* @return the source contents, which will never be null if {@code read} is
* true, or null if {@code read} is false and the source contents
* hasn't already been read.
*/
@Nullable
public String getSourceContents(boolean read) {
if (read) {
return getSourceContents();
} else {
return mSourceContents;
}
}
/**
* Returns a location for the given source line number in this class file's
* source file, if available.
*
* @param line the line number (1-based, which is what ASM uses)
* @param patternStart optional pattern to search for in the source for
* range start
* @param patternEnd optional pattern to search for in the source for range
* end
* @param hints additional hints about the pattern search (provided
* {@code patternStart} is non null)
* @return a location, never null
*/
@NonNull
public Location getLocationForLine(int line, @Nullable String patternStart,
@Nullable String patternEnd, @Nullable SearchHints hints) {
File sourceFile = getSourceFile();
if (sourceFile != null) {
// ASM line numbers are 1-based, and lint line numbers are 0-based
if (line != -1) {
return Location.create(sourceFile, getSourceContents(), line - 1,
patternStart, patternEnd, hints);
} else {
return Location.create(sourceFile);
}
}
return Location.create(file);
}
/**
* Reports an issue.
* <p>
* Detectors should only call this method if an error applies to the whole class
* scope and there is no specific method or field that applies to the error.
* If so, use
* {@link #report(Issue, org.objectweb.asm.tree.MethodNode, org.objectweb.asm.tree.AbstractInsnNode, Location, String)} or
* {@link #report(Issue, org.objectweb.asm.tree.FieldNode, Location, String)}, such that
* suppress annotations are checked.
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
if (mDriver.isSuppressed(issue, mClassNode)) {
return;
}
ClassNode curr = mClassNode;
while (curr != null) {
ClassNode prev = curr;
curr = mDriver.getOuterClassNode(curr);
if (curr != null) {
if (prev.outerMethod != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = curr.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.equals(prev.outerMethod)
&& method.desc.equals(prev.outerMethodDesc)) {
// Found the outer method for this anonymous class; continue
// reporting on it (which will also work its way up the parent
// class hierarchy)
if (method != null && mDriver.isSuppressed(issue, mClassNode, method,
null)) {
return;
}
break;
}
}
}
if (mDriver.isSuppressed(issue, curr)) {
return;
}
}
}
super.report(issue, location, message);
}
// Unfortunately, ASMs nodes do not extend a common DOM node type with parent
// pointers, so we have to have multiple methods which pass in each type
// of node (class, method, field) to be checked.
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param method the method scope the error applies to. The lint
* infrastructure will check whether there are suppress
* annotations on this method (or its enclosing class) and if so
* suppress the warning without involving the client.
* @param instruction the instruction within the method the error applies
* to. You cannot place annotations on individual method
* instructions (for example, annotations on local variables are
* allowed, but are not kept in the .class file). However, this
* instruction is needed to handle suppressing errors on field
* initializations; in that case, the errors may be reported in
* the {@code <clinit>} method, but the annotation is found not
* on that method but for the {@link FieldNode}'s.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull String message) {
if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Reports an issue applicable to a given method node.
*
* @param issue the issue to report
* @param field the scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this field (or its enclosing
* class) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull String message) {
if (field != null && mDriver.isSuppressed(issue, field)) {
return;
}
report(issue, location, message); // also checks the class node
}
/**
* Report an error.
* Like {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable MethodNode method,
@Nullable AbstractInsnNode instruction,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, method, instruction, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, FieldNode, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, FieldNode, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable FieldNode field,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, field, location, message);
}
/**
* Finds the line number closest to the given node
*
* @param node the instruction node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull AbstractInsnNode node) {
AbstractInsnNode curr = node;
// First search backwards
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getPrevious();
}
// Then search forwards
curr = node;
while (curr != null) {
if (curr.getType() == AbstractInsnNode.LINE) {
return ((LineNumberNode) curr).line;
}
curr = curr.getNext();
}
return -1;
}
/**
* Finds the line number closest to the given method declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull MethodNode node) {
if (node.instructions != null && node.instructions.size() > 0) {
return findLineNumber(node.instructions.get(0));
}
return -1;
}
/**
* Finds the line number closest to the given class declaration
*
* @param node the method node to get a line number for
* @return the closest line number, or -1 if not known
*/
public static int findLineNumber(@NonNull ClassNode node) {
if (node.methods != null && !node.methods.isEmpty()) {
MethodNode firstMethod = getFirstRealMethod(node);
if (firstMethod != null) {
return findLineNumber(firstMethod);
}
}
return -1;
}
/**
* Returns a location for the given {@link ClassNode}, where class node is
* either the top level class, or an inner class, in the current context.
*
* @param classNode the class in the current context
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName;
} else {
pattern = classNode.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
return getLocationForLine(findLineNumber(classNode), pattern, null,
SearchHints.create(BACKWARD).matchJavaSymbol());
}
@Nullable
private static MethodNode getFirstRealMethod(@NonNull ClassNode classNode) {
// Return the first method in the class for line number purposes. Skip <init>,
// since it's typically not located near the real source of the method.
if (classNode.methods != null) {
@SuppressWarnings("rawtypes") // ASM API
List methods = classNode.methods;
for (Object m : methods) {
MethodNode method = (MethodNode) m;
if (method.name.charAt(0) != '<') {
return method;
}
}
if (!classNode.methods.isEmpty()) {
return (MethodNode) classNode.methods.get(0);
}
}
return null;
}
/**
* Returns a location for the given {@link MethodNode}.
*
* @param methodNode the class in the current context
* @param classNode the class containing the method
* @return a location pointing to the class declaration, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull MethodNode methodNode,
@NonNull ClassNode classNode) {
// Attempt to find a proper location for this class. This is tricky
// since classes do not have line number entries in the class file; we need
// to find a method, look up the corresponding line number then search
// around it for a suitable tag, such as the class name.
String pattern;
SearchDirection searchMode;
if (methodNode.name.equals(CONSTRUCTOR_NAME)) {
searchMode = EOL_BACKWARD;
if (isAnonymousClass(classNode.name)) {
pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1);
} else {
pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1);
}
} else {
searchMode = BACKWARD;
pattern = methodNode.name;
}
return getLocationForLine(findLineNumber(methodNode), pattern, null,
SearchHints.create(searchMode).matchJavaSymbol());
}
/**
* Returns a location for the given {@link AbstractInsnNode}.
*
* @param instruction the instruction to look up the location for
* @return a location pointing to the instruction, or as close to it
* as possible
*/
@NonNull
public Location getLocation(@NonNull AbstractInsnNode instruction) {
SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol();
String pattern = null;
if (instruction instanceof MethodInsnNode) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.name.equals(CONSTRUCTOR_NAME)) {
pattern = call.owner;
hints = hints.matchConstructor();
} else {
pattern = call.name;
}
int index = pattern.lastIndexOf('$');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
index = pattern.lastIndexOf('/');
if (index != -1) {
pattern = pattern.substring(index + 1);
}
}
int line = findLineNumber(instruction);
return getLocationForLine(line, pattern, null, hints);
}
private static boolean isAnonymousClass(@NonNull String fqcn) {
int lastIndex = fqcn.lastIndexOf('$');
if (lastIndex != -1 && lastIndex < fqcn.length() - 1) {
if (Character.isDigit(fqcn.charAt(lastIndex + 1))) {
return true;
}
}
return false;
}
/**
* Converts from a VM owner name (such as foo/bar/Foo$Baz) to a
* fully qualified class name (such as foo.bar.Foo.Baz).
*
* @param owner the owner name to convert
* @return the corresponding fully qualified class name
*/
@NonNull
public static String getFqcn(@NonNull String owner) {
return owner.replace('/', '.').replace('$','.');
}
/**
* Computes a user-readable type signature from the given class owner, name
* and description. For example, for owner="foo/bar/Foo$Baz", name="foo",
* description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)".
*
* @param owner the class name
* @param name the method name
* @param desc the method description
* @return a user-readable string
*/
public static String createSignature(String owner, String name, String desc) {
StringBuilder sb = new StringBuilder(100);
if (desc != null) {
Type returnType = Type.getReturnType(desc);
sb.append(getTypeString(returnType));
sb.append(' ');
}
if (owner != null) {
sb.append(getFqcn(owner));
}
if (name != null) {
sb.append('#');
sb.append(name);
if (desc != null) {
Type[] argumentTypes = Type.getArgumentTypes(desc);
if (argumentTypes != null && argumentTypes.length > 0) {
sb.append('(');
boolean first = true;
for (Type type : argumentTypes) {
if (first) {
first = false;
} else {
sb.append(", ");
}
sb.append(getTypeString(type));
}
sb.append(')');
}
}
}
return sb.toString();
}
private static String getTypeString(Type type) {
String s = type.getClassName();
if (s.startsWith("java.lang.")) { //$NON-NLS-1$
s = s.substring("java.lang.".length()); //$NON-NLS-1$
}
return s;
}
/**
* Computes the internal class name of the given fully qualified class name.
* For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar
*
* @param fqcn the fully qualified class name
* @return the internal class name
*/
@NonNull
public static String getInternalName(@NonNull String fqcn) {
if (fqcn.indexOf('.') == -1) {
return fqcn;
}
// If class name contains $, it's not an ambiguous inner class name.
if (fqcn.indexOf('$') != -1) {
return fqcn.replace('.', '/');
}
// Let's assume that components that start with Caps are class names.
StringBuilder sb = new StringBuilder(fqcn.length());
String prev = null;
for (String part : Splitter.on('.').split(fqcn)) {
if (prev != null && !prev.isEmpty()) {
if (Character.isUpperCase(prev.charAt(0))) {
sb.append('$');
} else {
sb.append('/');
}
}
sb.append(part);
prev = part;
}
return sb.toString();
}
}
@@ -0,0 +1,441 @@
/*
* 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.detector.api;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.SUPPRESS_ALL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
* the detectors can share results), etc.
* <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 Context {
/**
* The file being checked. Note that this may not always be to a concrete
* file. For example, in the {@link Detector#beforeCheckProject(Context)}
* method, the context file is the directory of the project.
*/
public final File file;
/** The driver running through the checks */
protected final LintDriver mDriver;
/** The project containing the file being checked */
@NonNull
private final Project mProject;
/**
* The "main" project. For normal projects, this is the same as {@link #mProject},
* but for library projects, it's the root project that includes (possibly indirectly)
* the various library projects and their library projects.
* <p>
* Note that this is a property on the {@link Context}, not the
* {@link Project}, since a library project can be included from multiple
* different top level projects, so there isn't <b>one</b> main project,
* just one per main project being analyzed with its library projects.
*/
private final Project mMainProject;
/** The current configuration controlling which checks are enabled etc */
private final Configuration mConfiguration;
/** The contents of the file */
private String mContents;
/** Map of properties to share results between detectors */
private Map<String, Object> mProperties;
/** Whether this file contains any suppress markers (null means not yet determined) */
private Boolean mContainsCommentSuppress;
/**
* Construct a new {@link Context}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
*/
public Context(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
this.file = file;
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration();
}
/**
* Returns the scope for the lint job
*
* @return the scope, never null
*/
@NonNull
public EnumSet<Scope> getScope() {
return mDriver.getScope();
}
/**
* Returns the configuration for this project.
*
* @return the configuration, never null
*/
@NonNull
public Configuration getConfiguration() {
return mConfiguration;
}
/**
* Returns the project containing the file being checked
*
* @return the project, never null
*/
@NonNull
public Project getProject() {
return mProject;
}
/**
* Returns the main project if this project is a library project, or self
* if this is not a library project. The main project is the root project
* of all library projects, not necessarily the directly including project.
*
* @return the main project, never null
*/
@NonNull
public Project getMainProject() {
return mMainProject != null ? mMainProject : mProject;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mDriver.getClient();
}
/**
* Returns the driver running through the lint checks
*
* @return the driver
*/
@NonNull
public LintDriver getDriver() {
return mDriver;
}
/**
* Returns the contents of the file. This may not be the contents of the
* file on disk, since it delegates to the {@link LintClient}, which in turn
* may decide to return the current edited contents of the file open in an
* editor.
*
* @return the contents of the given file, or null if an error occurs.
*/
@Nullable
public String getContents() {
if (mContents == null) {
mContents = mDriver.getClient().readFile(file);
}
return mContents;
}
/**
* Returns the value of the given named property, or null.
*
* @param name the name of the property
* @return the corresponding value, or null
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
@Nullable
public Object getProperty(String name) {
if (mProperties == null) {
return null;
}
return mProperties.get(name);
}
/**
* Sets the value of the given named property.
*
* @param name the name of the property
* @param value the corresponding value
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
public void setProperty(@NonNull String name, @Nullable Object value) {
if (value == null) {
if (mProperties != null) {
mProperties.remove(name);
}
} else {
if (mProperties == null) {
mProperties = new HashMap<String, Object>();
}
mProperties.put(name, value);
}
}
/**
* Gets the SDK info for the current project.
*
* @return the SDK info for the current project, never null
*/
@NonNull
public SdkInfo getSdkInfo() {
return mProject.getSdkInfo();
}
// ---- Convenience wrappers ---- (makes the detector code a bit leaner)
/**
* Returns false if the given issue has been disabled. Convenience wrapper
* around {@link Configuration#getSeverity(Issue)}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return mConfiguration.isEnabled(issue);
}
/**
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
Configuration configuration = mConfiguration;
// If this error was computed for a context where the context corresponds to
// a project instead of a file, the actual error may be in a different project (e.g.
// a library project), so adjust the configuration as necessary.
if (location != null && location.getFile() != null) {
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration();
}
}
// If an error occurs in a library project, but you've disabled that check in the
// main project, disable it in the library project too. (In some cases you don't
// control the lint.xml of a library project, and besides, if you're not interested in
// a check for your main project you probably don't care about it in the library either.)
if (configuration != mConfiguration
&& mConfiguration.getSeverity(issue) == Severity.IGNORE) {
return;
}
Severity severity = configuration.getSeverity(issue);
if (severity == Severity.IGNORE) {
return;
}
mDriver.getClient().report(this, issue, severity, location, message, TextFormat.RAW);
}
/**
* Report an error.
* Like {@link #report(Issue, Location, String)} but with
* a now-unused data parameter at the end
*
* @deprecated Use {@link #report(Issue, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
}
/**
* Send an exception to the log. Convenience wrapper around {@link LintClient#log}.
*
* @param exception the exception, possibly null
* @param format the error message using {@link String#format} syntax, possibly null
* @param args any arguments for the format string
*/
public void log(
@Nullable Throwable exception,
@Nullable String format,
@Nullable Object... args) {
mDriver.getClient().log(exception, format, args);
}
/**
* Returns the current phase number. The first pass is numbered 1. Only one pass
* will be performed, unless a {@link Detector} calls {@link #requestRepeat}.
*
* @return the current phase, usually 1
*/
public int getPhase() {
return mDriver.getPhase();
}
/**
* Requests another pass through the data for the given detector. This is
* typically done when a detector needs to do more expensive computation,
* but it only wants to do this once it <b>knows</b> that an error is
* present, or once it knows more specifically what to check for.
*
* @param detector the detector that should be included in the next pass.
* Note that the lint runner may refuse to run more than a couple
* of runs.
* @param scope the scope to be revisited. This must be a subset of the
* current scope ({@link #getScope()}, and it is just a performance hint;
* in particular, the detector should be prepared to be called on other
* scopes as well (since they may have been requested by other detectors).
* You can pall null to indicate "all".
*/
public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) {
mDriver.requestRepeat(detector, scope);
}
/** Returns the comment marker used in Studio to suppress statements for language, if any */
@Nullable
protected String getSuppressCommentPrefix() {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(".cfg") || path.endsWith(".pro")) {
return "#suppress ";
}
return null;
}
/** Returns whether this file contains any suppress comment markers */
public boolean containsCommentSuppress() {
if (mContainsCommentSuppress == null) {
mContainsCommentSuppress = false;
String prefix = getSuppressCommentPrefix();
if (prefix != null) {
String contents = getContents();
if (contents != null) {
mContainsCommentSuppress = contents.contains(prefix);
}
}
}
return mContainsCommentSuppress;
}
/**
* Returns true if the given issue is suppressed at the given character offset
* in the file's contents
*/
public boolean isSuppressedWithComment(int startOffset, @NonNull Issue issue) {
String prefix = getSuppressCommentPrefix();
if (prefix == null) {
return false;
}
if (startOffset <= 0) {
return false;
}
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
if (startOffset >= contents.length()) {
return false;
}
// Scan backwards to the previous line and see if it contains the marker
int lineStart = contents.lastIndexOf('\n', startOffset) + 1;
if (lineStart <= 1) {
return false;
}
int index = findPrefixOnPreviousLine(contents, lineStart, prefix);
if (index != -1 &&index+prefix.length() < lineStart) {
String line = contents.substring(index + prefix.length(), lineStart);
return line.contains(issue.getId())
|| line.contains(SUPPRESS_ALL) && line.trim().startsWith(SUPPRESS_ALL);
}
return false;
}
private static int findPrefixOnPreviousLine(String contents, int lineStart, String prefix) {
// Search backwards on the previous line until you find the prefix start (also look
// back on previous lines if the previous line(s) contain just whitespace
char first = prefix.charAt(0);
int offset = lineStart - 2; // 0: first char on this line, -1: \n on previous line, -2 last
boolean seenNonWhitespace = false;
for (; offset >= 0; offset--) {
char c = contents.charAt(offset);
if (seenNonWhitespace && c == '\n') {
return -1;
}
if (!seenNonWhitespace && !Character.isWhitespace(c)) {
seenNonWhitespace = true;
}
if (c == first && contents.regionMatches(false, offset, prefix, 0,
prefix.length())) {
return offset;
}
}
return -1;
}
}
@@ -0,0 +1,68 @@
/*
* 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.detector.api;
import com.google.common.annotations.Beta;
/**
* A simple offset-based position *
* <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 DefaultPosition extends Position {
/** The line number (0-based where the first line is line 0) */
private final int mLine;
/**
* The column number (where the first character on the line is 0), or -1 if
* unknown
*/
private final int mColumn;
/** The character offset */
private final int mOffset;
/**
* Creates a new {@link DefaultPosition}
*
* @param line the 0-based line number, or -1 if unknown
* @param column the 0-based column number, or -1 if unknown
* @param offset the offset, or -1 if unknown
*/
public DefaultPosition(int line, int column, int offset) {
mLine = line;
mColumn = column;
mOffset = offset;
}
@Override
public int getLine() {
return mLine;
}
@Override
public int getOffset() {
return mOffset;
}
@Override
public int getColumn() {
return mColumn;
}
}
@@ -0,0 +1,778 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.JavaParser.ResolvedMethod;
import com.android.tools.lint.client.api.LintDriver;
import com.google.common.annotations.Beta;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import lombok.ast.AstVisitor;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorInvocation;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
/**
* A detector is able to find a particular problem (or a set of related problems).
* Each problem type is uniquely identified as an {@link Issue}.
* <p>
* Detectors will be called in a predefined order:
* <ol>
* <li> Manifest file
* <li> Resource files, in alphabetical order by resource type
* (therefore, "layout" is checked before "values", "values-de" is checked before
* "values-en" but after "values", and so on.
* <li> Java sources
* <li> Java classes
* <li> Gradle files
* <li> Generic files
* <li> Proguard files
* <li> Property files
* </ol>
* If a detector needs information when processing a file type that comes from a type of
* file later in the order above, they can request a second phase; see
* {@link LintDriver#requestRepeat}.
* <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 abstract class Detector {
/** Specialized interface for detectors that scan Java source file parse trees */
public interface JavaScanner {
/**
* Create a parse tree visitor to process the parse tree. All
* {@link JavaScanner} detectors must provide a visitor, unless they
* either return true from {@link #appliesToResourceRefs()} or return
* non null from {@link #getApplicableMethodNames()}.
* <p>
* If you return specific AST node types from
* {@link #getApplicableNodeTypes()}, then the visitor will <b>only</b>
* be called for the specific requested node types. This is more
* efficient, since it allows many detectors that apply to only a small
* part of the AST (such as method call nodes) to share iteration of the
* majority of the parse tree.
* <p>
* If you return null from {@link #getApplicableNodeTypes()}, then your
* visitor will be called from the top and all node types visited.
* <p>
* Note that a new visitor is created for each separate compilation
* unit, so you can store per file state in the visitor.
*
* @param context the {@link Context} for the file being analyzed
* @return a visitor, or null.
*/
@Nullable
AstVisitor createJavaVisitor(@NonNull JavaContext context);
/**
* Return the types of AST nodes that the visitor returned from
* {@link #createJavaVisitor(JavaContext)} should visit. See the
* documentation for {@link #createJavaVisitor(JavaContext)} for details
* on how the shared visitor is used.
* <p>
* If you return null from this method, then the visitor will process
* the full tree instead.
* <p>
* Note that for the shared visitor, the return codes from the visit
* methods are ignored: returning true will <b>not</b> prune iteration
* of the subtree, since there may be other node types interested in the
* children. If you need to ensure that your visitor only processes a
* part of the tree, use a full visitor instead. See the
* OverdrawDetector implementation for an example of this.
*
* @return the list of applicable node types (AST node classes), or null
*/
@Nullable
List<Class<? extends Node>> getApplicableNodeTypes();
/**
* Return the list of method names this detector is interested in, or
* null. If this method returns non-null, then any AST nodes that match
* a method call in the list will be passed to the
* {@link #visitMethod(JavaContext, AstVisitor, MethodInvocation)}
* method for processing. The visitor created by
* {@link #createJavaVisitor(JavaContext)} is also passed to that
* method, although it can be null.
* <p>
* This makes it easy to write detectors that focus on some fixed calls.
* For example, the StringFormatDetector uses this mechanism to look for
* "format" calls, and when found it looks around (using the AST's
* {@link Node#getParent()} method) to see if it's called on
* a String class instance, and if so do its normal processing. Note
* that since it doesn't need to do any other AST processing, that
* detector does not actually supply a visitor.
*
* @return a set of applicable method names, or null.
*/
@Nullable
List<String> getApplicableMethodNames();
/**
* Method invoked for any method calls found that matches any names
* returned by {@link #getApplicableMethodNames()}. This also passes
* back the visitor that was created by
* {@link #createJavaVisitor(JavaContext)}, but a visitor is not
* required. It is intended for detectors that need to do additional AST
* processing, but also want the convenience of not having to look for
* method names on their own.
*
* @param context the context of the lint request
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the {@link MethodInvocation} node for the invoked method
*/
void visitMethod(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull MethodInvocation node);
/**
* Return the list of constructor types this detector is interested in, or
* null. If this method returns non-null, then any AST nodes that match
* a constructor call in the list will be passed to the
* {@link #visitConstructor(JavaContext, AstVisitor, ConstructorInvocation, ResolvedMethod)}
* method for processing. The visitor created by
* {@link #createJavaVisitor(JavaContext)} is also passed to that
* method, although it can be null.
* <p>
* This makes it easy to write detectors that focus on some fixed constructors.
*
* @return a set of applicable fully qualified types, or null.
*/
@Nullable
List<String> getApplicableConstructorTypes();
/**
* Method invoked for any constructor calls found that matches any names
* returned by {@link #getApplicableConstructorTypes()}. This also passes
* back the visitor that was created by
* {@link #createJavaVisitor(JavaContext)}, but a visitor is not
* required. It is intended for detectors that need to do additional AST
* processing, but also want the convenience of not having to look for
* method names on their own.
*
* @param context the context of the lint request
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the {@link ConstructorInvocation} node for the invoked method
* @param constructor the resolved constructor method with type information
*/
void visitConstructor(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node,
@NonNull ResolvedMethod constructor);
/**
* Returns whether this detector cares about Android resource references
* (such as {@code R.layout.main} or {@code R.string.app_name}). If it
* does, then the visitor will look for these patterns, and if found, it
* will invoke {@link #visitResourceReference} passing the resource type
* and resource name. It also passes the visitor, if any, that was
* created by {@link #createJavaVisitor(JavaContext)}, such that a
* detector can do more than just look for resources.
*
* @return true if this detector wants to be notified of R resource
* identifiers found in the code.
*/
boolean appliesToResourceRefs();
/**
* Called for any resource references (such as {@code R.layout.main}
* found in Java code, provided this detector returned {@code true} from
* {@link #appliesToResourceRefs()}.
*
* @param context the lint scanning context
* @param visitor the visitor created from
* {@link #createJavaVisitor(JavaContext)}, or null
* @param node the variable reference for the resource
* @param type the resource type, such as "layout" or "string"
* @param name the resource name, such as "main" from
* {@code R.layout.main}
* @param isFramework whether the resource is a framework resource
* (android.R) or a local project resource (R)
*/
void visitResourceReference(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull Node node,
@NonNull String type,
@NonNull String name,
boolean isFramework);
/**
* Returns a list of fully qualified names for super classes that this
* detector cares about. If not null, this detector will *only* be called
* if the current class is a subclass of one of the specified superclasses.
*
* @return a list of fully qualified names
*/
@Nullable
List<String> applicableSuperClasses();
/**
* Called for each class that extends one of the super classes specified with
* {@link #applicableSuperClasses()}
*
* @param context the lint scanning context
* @param declaration the class declaration node, or null for anonymous classes
* @param node the class declaration node or the anonymous class construction node
* @param resolvedClass the resolved class
*/
void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull ResolvedClass resolvedClass);
}
/** Specialized interface for detectors that scan Java class files */
public interface ClassScanner {
/**
* Checks the given class' bytecode for issues.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
*/
void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode);
/**
* Returns the list of node types (corresponding to the constants in the
* {@link AbstractInsnNode} class) that this scanner applies to. The
* {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)}
* method will be called for each match.
*
* @return an array containing all the node types this detector should be
* called for, or null if none.
*/
@Nullable
int[] getApplicableAsmNodeTypes();
/**
* Process a given instruction node, and register lint issues if
* applicable.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
* @param method the method node containing the call
* @param instruction the actual instruction
*/
void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull AbstractInsnNode instruction);
/**
* Return the list of method call names (in VM format, e.g. "<init>" for
* constructors, etc) for method calls this detector is interested in,
* or null. T his will be used to dispatch calls to
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* for only the method calls in owners that the detector is interested
* in.
* <p>
* <b>NOTE</b>: If you return non null from this method, then <b>only</b>
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* will be called if a suitable method is found;
* {@link #checkClass(ClassContext, ClassNode)} will not be called under
* any circumstances.
* <p>
* This makes it easy to write detectors that focus on some fixed calls,
* and allows lint to make a single pass over the bytecode over a class,
* and efficiently dispatch method calls to any detectors that are
* interested in it. Without this, each new lint check interested in a
* single method, would be doing a complete pass through all the
* bytecode instructions of the class via the
* {@link #checkClass(ClassContext, ClassNode)} method, which would make
* each newly added lint check make lint slower. Now a single dispatch
* map is used instead, and for each encountered call in the single
* dispatch, it looks up in the map which if any detectors are
* interested in the given call name, and dispatches to each one in
* turn.
*
* @return a list of applicable method names, or null.
*/
@Nullable
List<String> getApplicableCallNames();
/**
* Just like {@link Detector#getApplicableCallNames()}, but for the owner
* field instead. The
* {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)}
* method will be called for all {@link MethodInsnNode} instances where the
* owner field matches any of the members returned in this node.
* <p>
* Note that if your detector provides both a name and an owner, the
* method will be called for any nodes matching either the name <b>or</b>
* the owner, not only where they match <b>both</b>. Note also that it will
* be called twice - once for the name match, and (at least once) for the owner
* match.
*
* @return a list of applicable owner names, or null.
*/
@Nullable
List<String> getApplicableCallOwners();
/**
* Process a given method call node, and register lint issues if
* applicable. This is similar to the
* {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)}
* method, but has the additional advantage that it is only called for known
* method names or method owners, according to
* {@link #getApplicableCallNames()} and {@link #getApplicableCallOwners()}.
*
* @param context the context of the lint check, pointing to for example
* the file
* @param classNode the root class node
* @param method the method node containing the call
* @param call the actual method call node
*/
void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call);
}
/** Specialized interface for detectors that scan binary resource files */
public interface BinaryResourceScanner {
/**
* Called for each resource folder
*
* @param context the context for the resource file
*/
void checkBinaryResource(@NonNull ResourceContext context);
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
boolean appliesTo(@NonNull ResourceFolderType folderType);
}
/** Specialized interface for detectors that scan resource folders (the folder directory
* itself, not the individual files within it */
public interface ResourceFolderScanner {
/**
* Called for each resource folder
*
* @param context the context for the resource folder
* @param folderName the resource folder name
*/
void checkFolder(@NonNull ResourceContext context, @NonNull String folderName);
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
boolean appliesTo(@NonNull ResourceFolderType folderType);
}
/** Specialized interface for detectors that scan XML files */
public interface XmlScanner {
/**
* Visit the given document. The detector is responsible for its own iteration
* through the document.
* @param context information about the document being analyzed
* @param document the document to examine
*/
void visitDocument(@NonNull XmlContext context, @NonNull Document document);
/**
* Visit the given element.
* @param context information about the document being analyzed
* @param element the element to examine
*/
void visitElement(@NonNull XmlContext context, @NonNull Element element);
/**
* Visit the given element after its children have been analyzed.
* @param context information about the document being analyzed
* @param element the element to examine
*/
void visitElementAfter(@NonNull XmlContext context, @NonNull Element element);
/**
* Visit the given attribute.
* @param context information about the document being analyzed
* @param attribute the attribute node to examine
*/
void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute);
/**
* Returns the list of elements that this detector wants to analyze. If non
* null, this detector will be called (specifically, the
* {@link #visitElement} method) for each matching element in the document.
* <p>
* If this method returns null, and {@link #getApplicableAttributes()} also returns
* null, then the {@link #visitDocument} method will be called instead.
*
* @return a collection of elements, or null, or the special
* {@link XmlScanner#ALL} marker to indicate that every single
* element should be analyzed.
*/
@Nullable
Collection<String> getApplicableElements();
/**
* Returns the list of attributes that this detector wants to analyze. If non
* null, this detector will be called (specifically, the
* {@link #visitAttribute} method) for each matching attribute in the document.
* <p>
* If this method returns null, and {@link #getApplicableElements()} also returns
* null, then the {@link #visitDocument} method will be called instead.
*
* @return a collection of attributes, or null, or the special
* {@link XmlScanner#ALL} marker to indicate that every single
* attribute should be analyzed.
*/
@Nullable
Collection<String> getApplicableAttributes();
/**
* Special marker collection returned by {@link #getApplicableElements()} or
* {@link #getApplicableAttributes()} to indicate that the check should be
* invoked on all elements or all attributes
*/
@NonNull
List<String> ALL = new ArrayList<String>(0); // NOT Collections.EMPTY!
// We want to distinguish this from just an *empty* list returned by the caller!
}
/** Specialized interface for detectors that scan Gradle files */
public interface GradleScanner {
void visitBuildScript(@NonNull Context context, Map<String, Object> sharedData);
}
/** Specialized interface for detectors that scan other files */
public interface OtherFileScanner {
/**
* Returns the set of files this scanner wants to consider. If this includes
* {@link Scope#OTHER} then all source files will be checked. Note that the
* set of files will not just include files of the indicated type, but all files
* within the relevant source folder. For example, returning {@link Scope#JAVA_FILE}
* will not just return {@code .java} files, but also other resource files such as
* {@code .html} and other files found within the Java source folders.
* <p>
* Lint will call the {@link #run(Context)}} method when the file should be checked.
*
* @return set of scopes that define the types of source files the
* detector wants to consider
*/
@NonNull
EnumSet<Scope> getApplicableFiles();
}
/**
* Runs the detector. This method will not be called for certain specialized
* detectors, such as {@link XmlScanner} and {@link JavaScanner}, where
* there are specialized analysis methods instead such as
* {@link XmlScanner#visitElement(XmlContext, Element)}.
*
* @param context the context describing the work to be done
*/
public void run(@NonNull Context context) {
}
/**
* Returns true if this detector applies to the given file
*
* @param context the context to check
* @param file the file in the context to check
* @return true if this detector applies to the given context and file
*/
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return false;
}
/**
* Analysis is about to begin, perform any setup steps.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void beforeCheckProject(@NonNull Context context) {
}
/**
* Analysis has just been finished for the whole project, perform any
* cleanup or report issues that require project-wide analysis.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void afterCheckProject(@NonNull Context context) {
}
/**
* Analysis is about to begin for the given library project, perform any setup steps.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void beforeCheckLibraryProject(@NonNull Context context) {
}
/**
* Analysis has just been finished for the given library project, perform any
* cleanup or report issues that require library-project-wide analysis.
*
* @param context the context for the check referencing the project, lint
* client, etc
*/
public void afterCheckLibraryProject(@NonNull Context context) {
}
/**
* Analysis is about to be performed on a specific file, perform any setup
* steps.
* <p>
* Note: When this method is called at the beginning of checking an XML
* file, the context is guaranteed to be an instance of {@link XmlContext},
* and similarly for a Java source file, the context will be a
* {@link JavaContext} and so on.
*
* @param context the context for the check referencing the file to be
* checked, the project, etc.
*/
public void beforeCheckFile(@NonNull Context context) {
}
/**
* Analysis has just been finished for a specific file, perform any cleanup
* or report issues found
* <p>
* Note: When this method is called at the end of checking an XML
* file, the context is guaranteed to be an instance of {@link XmlContext},
* and similarly for a Java source file, the context will be a
* {@link JavaContext} and so on.
*
* @param context the context for the check referencing the file to be
* checked, the project, etc.
*/
public void afterCheckFile(@NonNull Context context) {
}
/**
* Returns the expected speed of this detector
*
* @return the expected speed of this detector
*/
@NonNull
public Speed getSpeed() {
return Speed.NORMAL;
}
/**
* Returns the expected speed of this detector.
* The issue parameter is made available for subclasses which analyze multiple issues
* and which need to distinguish implementation cost by issue. If the detector does
* not analyze multiple issues or does not vary in speed by issue type, just override
* {@link #getSpeed()} instead.
*
* @param issue the issue to look up the analysis speed for
* @return the expected speed of this detector
*/
@NonNull
public Speed getSpeed(@SuppressWarnings("UnusedParameters") @NonNull Issue issue) {
// If not overridden, this detector does not distinguish speed by issue type
return getSpeed();
}
// ---- Dummy implementations to make implementing XmlScanner easier: ----
@SuppressWarnings("javadoc")
public void visitDocument(@NonNull XmlContext context, @NonNull Document document) {
// This method must be overridden if your detector does
// not return something from getApplicableElements or
// getApplicableAttributes
assert false;
}
@SuppressWarnings("javadoc")
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// This method must be overridden if your detector returns
// tag names from getApplicableElements
assert false;
}
@SuppressWarnings("javadoc")
public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) {
}
@SuppressWarnings("javadoc")
public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
// This method must be overridden if your detector returns
// attribute names from getApplicableAttributes
assert false;
}
@SuppressWarnings("javadoc")
@Nullable
public Collection<String> getApplicableElements() {
return null;
}
@Nullable
@SuppressWarnings("javadoc")
public Collection<String> getApplicableAttributes() {
return null;
}
// ---- Dummy implementations to make implementing JavaScanner easier: ----
@Nullable @SuppressWarnings("javadoc")
public List<String> getApplicableMethodNames() {
return null;
}
@Nullable @SuppressWarnings("javadoc")
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
return null;
}
@Nullable @SuppressWarnings("javadoc")
public List<Class<? extends Node>> getApplicableNodeTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull MethodInvocation node) {
}
@SuppressWarnings("javadoc")
public boolean appliesToResourceRefs() {
return false;
}
@SuppressWarnings("javadoc")
public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor,
@NonNull Node node, @NonNull String type, @NonNull String name,
boolean isFramework) {
}
@Nullable
public List<String> applicableSuperClasses() {
return null;
}
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull ResolvedClass resolvedClass) {
}
@Nullable @SuppressWarnings("javadoc")
public List<String> getApplicableConstructorTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void visitConstructor(
@NonNull JavaContext context,
@Nullable AstVisitor visitor,
@NonNull ConstructorInvocation node,
@NonNull ResolvedMethod constructor) {
}
// ---- Dummy implementations to make implementing a ClassScanner easier: ----
@SuppressWarnings("javadoc")
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
}
@SuppressWarnings("javadoc")
@Nullable
public List<String> getApplicableCallNames() {
return null;
}
@SuppressWarnings("javadoc")
@Nullable
public List<String> getApplicableCallOwners() {
return null;
}
@SuppressWarnings("javadoc")
public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull MethodInsnNode call) {
}
@SuppressWarnings("javadoc")
@Nullable
public int[] getApplicableAsmNodeTypes() {
return null;
}
@SuppressWarnings("javadoc")
public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode,
@NonNull MethodNode method, @NonNull AbstractInsnNode instruction) {
}
// ---- Dummy implementations to make implementing an OtherFileScanner easier: ----
public boolean appliesToFolder(@NonNull Scope scope, @Nullable ResourceFolderType folderType) {
return false;
}
@NonNull
public EnumSet<Scope> getApplicableFiles() {
return Scope.OTHER_SCOPE;
}
// ---- Dummy implementations to make implementing an GradleScanner easier: ----
public void visitBuildScript(@NonNull Context context, Map<String, Object> sharedData) {
}
// ---- Dummy implementations to make implementing a resource folder scanner easier: ----
public void checkFolder(@NonNull ResourceContext context, @NonNull String folderName) {
}
// ---- Dummy implementations to make implementing a binary resource scanner easier: ----
public void checkBinaryResource(@NonNull ResourceContext context) {
}
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
}
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2013 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.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.util.EnumSet;
/**
* An {@linkplain Implementation} of an {@link Issue} maps to the {@link Detector}
* class responsible for analyzing the issue, as well as the {@link Scope} required
* by the detector to perform its analysis.
* <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 Implementation {
private final Class<? extends Detector> mClass;
private final EnumSet<Scope> mScope;
private EnumSet<Scope>[] mAnalysisScopes;
@SuppressWarnings("unchecked")
private static final EnumSet<Scope>[] EMPTY = new EnumSet[0];
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
*/
@SuppressWarnings("unchecked")
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope) {
this(detectorClass, scope, EMPTY);
}
/**
* Creates a new implementation for analyzing one or more issues
*
* @param detectorClass the class of the detector to find this issue
* @param scope the scope of files required to analyze this issue
* @param analysisScopes optional set of extra scopes the detector is capable of working in
*/
public Implementation(
@NonNull Class<? extends Detector> detectorClass,
@NonNull EnumSet<Scope> scope,
@NonNull EnumSet<Scope>... analysisScopes) {
mClass = detectorClass;
mScope = scope;
mAnalysisScopes = analysisScopes;
}
/**
* Returns the class of the detector to use to find this issue
*
* @return the class of the detector to use to find this issue
*/
@NonNull
public Class<? extends Detector> getDetectorClass() {
return mClass;
}
@Override
public String toString() {
return mClass.toString();
}
/**
* Returns the scope required to analyze the code to detect this issue.
* This is determined by the detectors which reports the issue.
*
* @return the required scope
*/
@NonNull
public EnumSet<Scope> getScope() {
return mScope;
}
/**
* Returns the sets of scopes required to analyze this issue, or null if all
* scopes named by {@link #getScope()} are necessary. Note that only
* <b>one</b> match out of this collection is required, not all, and that
* the scope set returned by {@link #getScope()} does not have to be returned
* by this method, but is always implied to be included.
* <p>
* The scopes returned by {@link #getScope()} list all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* The required scope list provides a list of scope sets that can be used to
* analyze this issue. For each scope set, all the scopes must be matched by
* the incremental analysis, but any one of the scope sets can be analyzed
* in isolation.
* <p>
* The required scope list is not required to include the full scope set
* returned by {@link #getScope()}; that set is always assumed to be
* included.
* <p>
* NOTE: You would normally call {@link #isAdequate(EnumSet)} rather
* than calling this method directly.
*
* @return a list of required scopes, or null.
*/
@NonNull
public EnumSet<Scope>[] getAnalysisScopes() {
return mAnalysisScopes;
}
/**
* Returns true if the given scope is adequate for analyzing this issue.
* This looks through the analysis scopes (see
* {@link #getAnalysisScopes()}) and if the scope passed in fully
* covers at least one of them, or if it covers the scope of the issue
* itself (see {@link #getScope()}, which should be a superset of all the
* analysis scopes) returns true.
* <p>
* The scope set returned by {@link #getScope()} lists all the various
* scopes that are <b>affected</b> by this issue, meaning the detector
* should consider it. Frequently, the detector must analyze all these
* scopes in order to properly decide whether an issue is found. For
* example, the unused resource detector needs to consider both the XML
* resource files and the Java source files in order to decide if a resource
* is unused. If it analyzes just the Java files for example, it might
* incorrectly conclude that a resource is unused because it did not
* discover a resource reference in an XML file.
* <p>
* However, there are other issues where the issue can occur in a variety of
* files, but the detector can consider each in isolation. For example, the
* API checker is affected by both XML files and Java class files (detecting
* both layout constructor references in XML layout files as well as code
* references in .class files). It doesn't have to analyze both; it is
* capable of incrementally analyzing just an XML file, or just a class
* file, without considering the other.
* <p>
* An issue can register additional scope sets that can are adequate
* for analyzing the issue, by supplying it to
* {@link #Implementation(Class, java.util.EnumSet, java.util.EnumSet[])}.
* This method returns true if the given scope matches one or more analysis
* scope, or the overall scope.
*
* @param scope the scope available for analysis
* @return true if this issue can be analyzed with the given available scope
*/
public boolean isAdequate(@NonNull EnumSet<Scope> scope) {
if (scope.containsAll(mScope)) {
return true;
}
if (mAnalysisScopes != null) {
for (EnumSet<Scope> analysisScope : mAnalysisScopes) {
if (scope.containsAll(analysisScope)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,307 @@
/*
* 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.detector.api;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import com.android.annotations.NonNull;
import com.android.tools.lint.client.api.Configuration;
import com.google.common.annotations.Beta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* An issue is a potential bug in an Android application. An issue is discovered
* by a {@link Detector}, and has an associated {@link Severity}.
* <p>
* Issues and detectors are separate classes because a detector can discover
* multiple different issues as it's analyzing code, and we want to be able to
* different severities for different issues, the ability to suppress one but
* not other issues from the same detector, and so on.
* <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 final class Issue implements Comparable<Issue> {
private final String mId;
private final String mBriefDescription;
private final String mExplanation;
private final Category mCategory;
private final int mPriority;
private final Severity mSeverity;
private Object mMoreInfoUrls;
private boolean mEnabledByDefault = true;
private Implementation mImplementation;
// Use factory methods
private Issue(
@NonNull String id,
@NonNull String shortDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
assert !shortDescription.isEmpty();
assert !explanation.isEmpty();
mId = id;
mBriefDescription = shortDescription;
mExplanation = explanation;
mCategory = category;
mPriority = priority;
mSeverity = severity;
mImplementation = implementation;
}
/**
* Creates a new issue. The description strings can use some simple markup;
* see the {@link TextFormat#RAW} documentation
* for details.
*
* @param id the fixed id of the issue
* @param briefDescription short summary (typically 5-6 words or less), typically
* describing the <b>problem</b> rather than the <b>fix</b>
* (e.g. "Missing minSdkVersion")
* @param explanation a full explanation of the issue, with suggestions for
* how to fix it
* @param category the associated category, if any
* @param priority the priority, a number from 1 to 10 with 10 being most
* important/severe
* @param severity the default severity of the issue
* @param implementation the default implementation for this issue
* @return a new {@link Issue}
*/
@NonNull
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* For compatibility with older custom rules)
*
* @deprecated Use {@link #create(String, String, String, Category, int, Severity, Implementation)} instead
*/
@NonNull
@Deprecated
public static Issue create(
@NonNull String id,
@NonNull String briefDescription,
@SuppressWarnings("UnusedParameters") @NonNull String description,
@NonNull String explanation,
@NonNull Category category,
int priority,
@NonNull Severity severity,
@NonNull Implementation implementation) {
return new Issue(id, briefDescription, explanation, category, priority,
severity, implementation);
}
/**
* Returns the unique id of this issue. These should not change over time
* since they are used to persist the names of issues suppressed by the user
* etc. It is typically a single camel-cased word.
*
* @return the associated fixed id, never null and always unique
*/
@NonNull
public String getId() {
return mId;
}
/**
* Briefly (in a couple of words) describes these errors
*
* @return a brief summary of the issue, never null, never empty
*/
@NonNull
public String getBriefDescription(@NonNull TextFormat format) {
return RAW.convertTo(mBriefDescription, format);
}
/**
* Describes the error found by this rule, e.g.
* "Buttons must define contentDescriptions". Preferably the explanation
* should also contain a description of how the problem should be solved.
* Additional info can be provided via {@link #getMoreInfo()}.
*
* @param format the format to write the format as
* @return an explanation of the issue, never null, never empty
*/
@NonNull
public String getExplanation(@NonNull TextFormat format) {
return RAW.convertTo(mExplanation, format);
}
/**
* The primary category of the issue
*
* @return the primary category of the issue, never null
*/
@NonNull
public Category getCategory() {
return mCategory;
}
/**
* Returns a priority, in the range 1-10, with 10 being the most severe and
* 1 the least
*
* @return a priority from 1 to 10
*/
public int getPriority() {
return mPriority;
}
/**
* Returns the default severity of the issues found by this detector (some
* tools may allow the user to specify custom severities for detectors).
* <p>
* Note that even though the normal way for an issue to be disabled is for
* the {@link Configuration} to return {@link Severity#IGNORE}, there is a
* {@link #isEnabledByDefault()} method which can be used to turn off issues
* by default. This is done rather than just having the severity as the only
* attribute on the issue such that an issue can be configured with an
* appropriate severity (such as {@link Severity#ERROR}) even when issues
* are disabled by default for example because they are experimental or not
* yet stable.
*
* @return the severity of the issues found by this detector
*/
@NonNull
public Severity getDefaultSeverity() {
return mSeverity;
}
/**
* Returns a link (a URL string) to more information, or null
*
* @return a link to more information, or null
*/
@NonNull
public List<String> getMoreInfo() {
if (mMoreInfoUrls == null) {
return Collections.emptyList();
} else if (mMoreInfoUrls instanceof String) {
return Collections.singletonList((String) mMoreInfoUrls);
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
return (List<String>) mMoreInfoUrls;
}
}
/**
* Adds a more info URL string
*
* @param moreInfoUrl url string
* @return this, for constructor chaining
*/
@NonNull
public Issue addMoreInfo(@NonNull String moreInfoUrl) {
// Nearly all issues supply at most a single URL, so don't bother with
// lists wrappers for most of these issues
if (mMoreInfoUrls == null) {
mMoreInfoUrls = moreInfoUrl;
} else if (mMoreInfoUrls instanceof String) {
String existing = (String) mMoreInfoUrls;
List<String> list = new ArrayList<String>(2);
list.add(existing);
list.add(moreInfoUrl);
mMoreInfoUrls = list;
} else {
assert mMoreInfoUrls instanceof List;
//noinspection unchecked
((List<String>) mMoreInfoUrls).add(moreInfoUrl);
}
return this;
}
/**
* Returns whether this issue should be enabled by default, unless the user
* has explicitly disabled it.
*
* @return true if this issue should be enabled by default
*/
public boolean isEnabledByDefault() {
return mEnabledByDefault;
}
/**
* Returns the implementation for the given issue
*
* @return the implementation for this issue
*/
@NonNull
public Implementation getImplementation() {
return mImplementation;
}
/**
* Sets the implementation for the given issue. This is typically done by
* IDEs that can offer a replacement for a given issue which performs better
* or in some other way works better within the IDE.
*
* @param implementation the new implementation to use
*/
public void setImplementation(@NonNull Implementation implementation) {
mImplementation = implementation;
}
/**
* Sorts the detectors alphabetically by id. This is intended to make it
* convenient to store settings for detectors in a fixed order. It is not
* intended as the order to be shown to the user; for that, a tool embedding
* lint might consider the priorities, categories, severities etc of the
* various detectors.
*
* @param other the {@link Issue} to compare this issue to
*/
@Override
public int compareTo(@NonNull Issue other) {
return getId().compareTo(other.getId());
}
/**
* Sets whether this issue is enabled by default.
*
* @param enabledByDefault whether the issue should be enabled by default
* @return this, for constructor chaining
*/
@NonNull
public Issue setEnabledByDefault(boolean enabledByDefault) {
mEnabledByDefault = enabledByDefault;
return this;
}
@Override
public String toString() {
return mId;
}
}
@@ -0,0 +1,394 @@
/*
* 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.detector.api;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.tools.lint.client.api.JavaParser.ResolvedNode;
import static com.android.tools.lint.client.api.JavaParser.TypeDescriptor;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.client.api.JavaParser.ResolvedClass;
import com.android.tools.lint.client.api.LintDriver;
import java.io.File;
import java.util.Iterator;
import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.Expression;
import lombok.ast.MethodDeclaration;
import lombok.ast.MethodInvocation;
import lombok.ast.Node;
import lombok.ast.Position;
/**
* A {@link Context} used when checking Java files.
* <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>
*/
public class JavaContext extends Context {
static final String SUPPRESS_COMMENT_PREFIX = "//noinspection "; //$NON-NLS-1$
/** The parse tree */
private Node mCompilationUnit;
/** The parser which produced the parse tree */
private final JavaParser mParser;
/**
* Constructs a {@link JavaContext} for running lint on the given file, with
* the given scope, in the given project reporting errors to the given
* client.
*
* @param driver the driver running through the checks
* @param project the project to run lint on which contains the given file
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file to be analyzed
* @param parser the parser to use
*/
public JavaContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@NonNull JavaParser parser) {
super(driver, project, main, file);
mParser = parser;
}
/**
* Returns a location for the given node
*
* @param node the AST node to get a location for
* @return a location for the given node
*/
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
@NonNull
public JavaParser getParser() {
return mParser;
}
@Nullable
public Node getCompilationUnit() {
return mCompilationUnit;
}
/**
* Sets the compilation result. Not intended for client usage; the lint infrastructure
* will set this when a context has been processed
*
* @param compilationUnit the parse tree
*/
public void setCompilationUnit(@Nullable Node compilationUnit) {
mCompilationUnit = compilationUnit;
}
@Override
public void report(@NonNull Issue issue, @Nullable Location location,
@NonNull String message) {
if (mDriver.isSuppressed(this, issue, mCompilationUnit)) {
return;
}
super.report(issue, location, message);
}
/**
* Reports an issue applicable to a given AST node. The AST node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the AST node scope the error applies to. The lint infrastructure
* will check whether there are suppress annotations on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
@Nullable
public static Node findSurroundingMethod(Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) {
return scope;
}
scope = scope.getParent();
}
return null;
}
@Nullable
public static ClassDeclaration findSurroundingClass(@Nullable Node scope) {
while (scope != null) {
Class<? extends Node> type = scope.getClass();
// The Lombok AST uses a flat hierarchy of node type implementation classes
// so no need to do instanceof stuff here.
if (type == ClassDeclaration.class) {
return (ClassDeclaration) scope;
}
scope = scope.getParent();
}
return null;
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
public boolean isSuppressedWithComment(@NonNull Node scope, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
Position position = scope.getPosition();
if (position == null) {
return false;
}
int start = position.getStart();
return isSuppressedWithComment(start, issue);
}
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
@Nullable
public ResolvedNode resolve(@NonNull Node node) {
return mParser.resolve(this, node);
}
@Nullable
public ResolvedClass findClass(@NonNull String fullyQualifiedName) {
return mParser.findClass(this, fullyQualifiedName);
}
@Nullable
public TypeDescriptor getType(@NonNull Node node) {
return mParser.getType(this, node);
}
/**
* Returns true if the given method invocation node corresponds to a call on a
* {@code android.content.Context}
*
* @param node the method call node
* @return true iff the method call is on a class extending context
*/
public boolean isContextMethod(@NonNull MethodInvocation node) {
// Method name used in many other contexts where it doesn't have the
// same semantics; only use this one if we can resolve types
// and we're certain this is the Context method
ResolvedNode resolved = resolve(node);
if (resolved instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) resolved;
ResolvedClass containingClass = method.getContainingClass();
if (containingClass.isSubclassOf(CLASS_CONTEXT, false)) {
return true;
}
}
return false;
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz) {
return getParentOfType(element, clz, true);
}
/**
* Returns the first ancestor node of the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(
@Nullable Node element,
@NonNull Class<T> clz,
boolean strict) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null) {
if (clz.isInstance(element)) {
//noinspection unchecked
return (T) element;
}
element = element.getParent();
}
return null;
}
/**
* Returns the first ancestor node of the given type, stopping at the given type
*
* @param element the element to search from
* @param clz the target node type
* @param strict if true, do not consider the element itself, only its parents
* @param terminators optional node types to terminate the search at
* @param <T> the target node type
* @return the nearest ancestor node in the parent chain, or null if not found
*/
@Nullable
public static <T extends Node> T getParentOfType(@Nullable Node element,
@NonNull Class<T> clz,
boolean strict,
@NonNull Class<? extends Node>... terminators) {
if (element == null) {
return null;
}
if (strict) {
element = element.getParent();
}
while (element != null && !clz.isInstance(element)) {
for (Class<?> terminator : terminators) {
if (terminator.isInstance(element)) {
return null;
}
}
element = element.getParent();
}
//noinspection unchecked
return (T) element;
}
/**
* Returns the first sibling of the given node that is of the given class
*
* @param sibling the sibling to search from
* @param clz the type to look for
* @param <T> the type
* @return the first sibling of the given type, or null
*/
@Nullable
public static <T extends Node> T getNextSiblingOfType(@Nullable Node sibling,
@NonNull Class<T> clz) {
if (sibling == null) {
return null;
}
Node parent = sibling.getParent();
if (parent == null) {
return null;
}
Iterator<Node> iterator = parent.getChildren().iterator();
while (iterator.hasNext()) {
if (iterator.next() == sibling) {
break;
}
}
while (iterator.hasNext()) {
Node child = iterator.next();
if (clz.isInstance(child)) {
//noinspection unchecked
return (T) child;
}
}
return null;
}
/**
* Returns the given argument of the given call
*
* @param call the call containing arguments
* @param index the index of the target argument
* @return the argument at the given index
* @throws IllegalArgumentException if index is outside the valid range
*/
@NonNull
public static Node getArgumentNode(@NonNull MethodInvocation call, int index) {
int i = 0;
for (Expression parameter : call.astArguments()) {
if (i == index) {
return parameter;
}
i++;
}
throw new IllegalArgumentException(Integer.toString(index));
}
}
@@ -0,0 +1,70 @@
/*
* 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.detector.api;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
import static com.android.SdkConstants.ATTR_PADDING;
import static com.android.SdkConstants.ATTR_PADDING_BOTTOM;
import static com.android.SdkConstants.ATTR_PADDING_LEFT;
import static com.android.SdkConstants.ATTR_PADDING_RIGHT;
import static com.android.SdkConstants.ATTR_PADDING_TOP;
import static com.android.SdkConstants.VALUE_FILL_PARENT;
import static com.android.SdkConstants.VALUE_MATCH_PARENT;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import org.w3c.dom.Element;
/**
* Abstract class specifically intended for layout detectors which provides some
* common utility methods shared by layout detectors.
* <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 abstract class LayoutDetector extends ResourceXmlDetector {
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT;
}
private static boolean isFillParent(@NonNull Element element, @NonNull String dimension) {
String width = element.getAttributeNS(ANDROID_URI, dimension);
return width.equals(VALUE_MATCH_PARENT) || width.equals(VALUE_FILL_PARENT);
}
protected static boolean isWidthFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_WIDTH);
}
protected static boolean isHeightFillParent(@NonNull Element element) {
return isFillParent(element, ATTR_LAYOUT_HEIGHT);
}
protected boolean hasPadding(@NonNull Element root) {
return root.hasAttributeNS(ANDROID_URI, ATTR_PADDING)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP)
|| root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,777 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Location information for a warning
* <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 Location {
private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$
private final File mFile;
private final Position mStart;
private final Position mEnd;
private String mMessage;
private Location mSecondary;
private Object mClientData;
/**
* (Private constructor, use one of the factory methods
* {@link Location#create(File)},
* {@link Location#create(File, Position, Position)}, or
* {@link Location#create(File, String, int, int)}.
* <p>
* Constructs a new location range for the given file, from start to end. If
* the length of the range is not known, end may be null.
*
* @param file the associated file (but see the documentation for
* {@link #getFile()} for more information on what the file
* represents)
* @param start the starting position, or null
* @param end the ending position, or null
*/
protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) {
super();
mFile = file;
mStart = start;
mEnd = end;
}
/**
* Returns the file containing the warning. Note that the file *itself* may
* not yet contain the error. When editing a file in the IDE for example,
* the tool could generate warnings in the background even before the
* document is saved. However, the file is used as a identifying token for
* the document being edited, and the IDE integration can map this back to
* error locations in the editor source code.
*
* @return the file handle for the location
*/
@NonNull
public File getFile() {
return mFile;
}
/**
* The start position of the range
*
* @return the start position of the range, or null
*/
@Nullable
public Position getStart() {
return mStart;
}
/**
* The end position of the range
*
* @return the start position of the range, may be null for an empty range
*/
@Nullable
public Position getEnd() {
return mEnd;
}
/**
* Returns a secondary location associated with this location (if
* applicable), or null.
*
* @return a secondary location or null
*/
@Nullable
public Location getSecondary() {
return mSecondary;
}
/**
* Sets a secondary location for this location.
*
* @param secondary a secondary location associated with this location
*/
public void setSecondary(@Nullable Location secondary) {
mSecondary = secondary;
}
/**
* Sets a custom message for this location. This is typically used for
* secondary locations, to describe the significance of this alternate
* location. For example, for a duplicate id warning, the primary location
* might say "This is a duplicate id", pointing to the second occurrence of
* id declaration, and then the secondary location could point to the
* original declaration with the custom message "Originally defined here".
*
* @param message the message to apply to this location
*/
public void setMessage(@NonNull String message) {
mMessage = message;
}
/**
* Returns the custom message for this location, if any. This is typically
* used for secondary locations, to describe the significance of this
* alternate location. For example, for a duplicate id warning, the primary
* location might say "This is a duplicate id", pointing to the second
* occurrence of id declaration, and then the secondary location could point
* to the original declaration with the custom message
* "Originally defined here".
*
* @return the custom message for this location, or null
*/
@Nullable
public String getMessage() {
return mMessage;
}
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
public Object getClientData() {
return mClientData;
}
@Override
public String toString() {
return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message="
+ mMessage + ']';
}
/**
* Creates a new location for the given file
*
* @param file the file to create a location for
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file) {
return new Location(file, null /*start*/, null /*end*/);
}
/**
* Creates a new location for the given file and SourcePosition.
*
* @param file the file containing the positions
* @param position the source position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull SourcePosition position) {
if (position.equals(SourcePosition.UNKNOWN)) {
return new Location(file, null /*start*/, null /*end*/);
}
return new Location(file,
new DefaultPosition(
position.getStartLine(),
position.getStartColumn(),
position.getStartOffset()),
new DefaultPosition(
position.getEndLine(),
position.getEndColumn(),
position.getEndOffset()));
}
/**
* Creates a new location for the given file and starting and ending
* positions.
*
* @param file the file containing the positions
* @param start the starting position
* @param end the ending position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull Position start,
@Nullable Position end) {
return new Location(file, start, end);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given offset range.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param startOffset the starting offset
* @param endOffset the ending offset
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@Nullable String contents,
int startOffset,
int endOffset) {
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("Invalid offsets");
}
if (contents == null) {
return new Location(file,
new DefaultPosition(-1, -1, startOffset),
new DefaultPosition(-1, -1, endOffset));
}
int size = contents.length();
endOffset = Math.min(endOffset, size);
startOffset = Math.min(startOffset, endOffset);
Position start = null;
int line = 0;
int lineOffset = 0;
char prev = 0;
for (int offset = 0; offset <= size; offset++) {
if (offset == startOffset) {
start = new DefaultPosition(line, offset - lineOffset, offset);
}
if (offset == endOffset) {
Position end = new DefaultPosition(line, offset - lineOffset, offset);
return new Location(file, start, end);
}
char c = contents.charAt(offset);
if (c == '\n') {
lineOffset = offset + 1;
if (prev != '\r') {
line++;
}
} else if (c == '\r') {
line++;
lineOffset = offset + 1;
}
prev = c;
}
return create(file);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line) {
return create(file, contents, line, null, null, null);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @param patternStart an optional pattern to search for from the line
* match; if found, adjust the column and offsets to begin at the
* pattern start
* @param patternEnd an optional pattern to search for behind the start
* pattern; if found, adjust the end offset to match the end of
* the pattern
* @param hints optional additional information regarding the pattern search
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line,
@Nullable String patternStart, @Nullable String patternEnd,
@Nullable SearchHints hints) {
int currentLine = 0;
int offset = 0;
while (currentLine < line) {
offset = contents.indexOf('\n', offset);
if (offset == -1) {
return create(file);
}
currentLine++;
offset++;
}
if (line == currentLine) {
if (patternStart != null) {
SearchDirection direction = SearchDirection.NEAREST;
if (hints != null) {
direction = hints.mDirection;
}
int index;
if (direction == SearchDirection.BACKWARD) {
index = findPreviousMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.EOL_BACKWARD) {
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
index = findPreviousMatch(contents, lineEnd, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.FORWARD) {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST;
int before = findPreviousMatch(contents, offset, patternStart, hints);
int after = findNextMatch(contents, offset, patternStart, hints);
if (before == -1) {
index = after;
line = adjustLine(contents, line, offset, index);
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else if (offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
}
}
if (index != -1) {
int lineStart = contents.lastIndexOf('\n', index);
if (lineStart == -1) {
lineStart = 0;
} else {
lineStart++; // was pointing to the previous line's CR, not line start
}
int column = index - lineStart;
if (patternEnd != null) {
int end = contents.indexOf(patternEnd, offset + patternStart.length());
if (end != -1) {
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, -1, end + patternEnd.length()));
}
} else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) {
if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) {
patternStart = SUPER_KEYWORD;
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column + patternStart.length(),
index + patternStart.length()));
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column, index + patternStart.length()));
}
}
Position position = new DefaultPosition(line, -1, offset);
return new Location(file, position, position);
}
return create(file);
}
private static int findPreviousMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
while (true) {
int index = contents.lastIndexOf(pattern, offset);
if (index == -1) {
return -1;
} else {
if (isMatch(contents, index, pattern, hints)) {
return index;
} else {
offset = index - pattern.length();
}
}
}
}
private static int findNextMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
int constructorIndex = -1;
if (hints != null && hints.isConstructor()) {
// Special condition: See if the call is referenced as "super" instead.
assert hints.isWholeWord();
int index = contents.indexOf(SUPER_KEYWORD, offset);
if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) {
constructorIndex = index;
}
}
while (true) {
int index = contents.indexOf(pattern, offset);
if (index == -1) {
return constructorIndex;
} else {
if (isMatch(contents, index, pattern, hints)) {
if (constructorIndex != -1) {
return Math.min(constructorIndex, index);
}
return index;
} else {
offset = index + pattern.length();
}
}
}
}
private static boolean isMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
if (!contents.startsWith(pattern, offset)) {
return false;
}
if (hints != null) {
char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0;
int lastIndex = offset + pattern.length() - 1;
char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0;
if (hints.isWholeWord() && (Character.isLetter(prevChar)
|| Character.isLetter(nextChar))) {
return false;
}
if (hints.isJavaSymbol()) {
if (Character.isJavaIdentifierPart(prevChar)
|| Character.isJavaIdentifierPart(nextChar)) {
return false;
}
if (prevChar == '"') {
return false;
}
// TODO: Additional validation to see if we're in a comment, string, etc.
// This will require lexing from the beginning of the buffer.
}
if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) {
// Only looking for super(), not super.x, so assert that the next
// non-space character is (
int index = lastIndex + 1;
while (index < contents.length() - 1) {
char c = contents.charAt(index);
if (c == '(') {
break;
} else if (!Character.isWhitespace(c)) {
return false;
}
index++;
}
}
}
return true;
}
private static int adjustLine(String doc, int line, int offset, int newOffset) {
if (newOffset == -1) {
return line;
}
if (newOffset < offset) {
return line - countLines(doc, newOffset, offset);
} else {
return line + countLines(doc, offset, newOffset);
}
}
private static int countLines(String doc, int start, int end) {
int lines = 0;
for (int offset = start; offset < end; offset++) {
char c = doc.charAt(offset);
if (c == '\n') {
lines++;
}
}
return lines;
}
/**
* Reverses the secondary location list initiated by the given location
*
* @param location the first location in the list
* @return the first location in the reversed list
*/
public static Location reverse(@NonNull Location location) {
Location next = location.getSecondary();
location.setSecondary(null);
while (next != null) {
Location nextNext = next.getSecondary();
next.setSecondary(location);
location = next;
next = nextNext;
}
return location;
}
/**
* A {@link Handle} is a reference to a location. The point of a location
* handle is to be able to create them cheaply, and then resolve them into
* actual locations later (if needed). This makes it possible to for example
* delay looking up line numbers, for locations that are offset based.
*/
public interface Handle {
/**
* Compute a full location for the given handle
*
* @return create a location for this handle
*/
@NonNull
Location resolve();
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
void setClientData(@Nullable Object clientData);
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
Object getClientData();
}
/** A default {@link Handle} implementation for simple file offsets */
public static class DefaultLocationHandle implements Handle {
private final File mFile;
private final String mContents;
private final int mStartOffset;
private final int mEndOffset;
private Object mClientData;
/**
* Constructs a new {@link DefaultLocationHandle}
*
* @param context the context pointing to the file and its contents
* @param startOffset the start offset within the file
* @param endOffset the end offset within the file
*/
public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) {
mFile = context.file;
mContents = context.getContents();
mStartOffset = startOffset;
mEndOffset = endOffset;
}
@Override
@NonNull
public Location resolve() {
return create(mFile, mContents, mStartOffset, mEndOffset);
}
@Override
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
@Override
@Nullable
public Object getClientData() {
return mClientData;
}
}
public static class ResourceItemHandle implements Handle {
private final ResourceItem mItem;
public ResourceItemHandle(@NonNull ResourceItem item) {
mItem = item;
}
@NonNull
@Override
public Location resolve() {
// TODO: Look up the exact item location more
// closely
ResourceFile source = mItem.getSource();
assert source != null : mItem;
return create(source.getFile());
}
@Override
public void setClientData(@Nullable Object clientData) {
}
@Nullable
@Override
public Object getClientData() {
return null;
}
}
/**
* Whether to look forwards, or backwards, or in both directions, when
* searching for a pattern in the source code to determine the right
* position range for a given symbol.
* <p>
* When dealing with bytecode for example, there are only line number entries
* within method bodies, so when searching for the method declaration, we should only
* search backwards from the first line entry in the method.
*/
public enum SearchDirection {
/** Only search forwards */
FORWARD,
/** Only search backwards */
BACKWARD,
/** Search backwards from the current end of line (normally it's the beginning of
* the current line) */
EOL_BACKWARD,
/**
* Search both forwards and backwards from the given line, and prefer
* the match that is closest
*/
NEAREST,
}
/**
* Extra information pertaining to finding a symbol in a source buffer,
* used by {@link Location#create(File, String, int, String, String, SearchHints)}
*/
public static class SearchHints {
/**
* the direction to search for the nearest match in (provided
* {@code patternStart} is non null)
*/
@NonNull
private final SearchDirection mDirection;
/** Whether the matched pattern should be a whole word */
private boolean mWholeWord;
/**
* Whether the matched pattern should be a Java symbol (so for example,
* a match inside a comment or string literal should not be used)
*/
private boolean mJavaSymbol;
/**
* Whether the matched pattern corresponds to a constructor; if so, look for
* some other possible source aliases too, such as "super".
*/
private boolean mConstructor;
private SearchHints(@NonNull SearchDirection direction) {
super();
mDirection = direction;
}
/**
* Constructs a new {@link SearchHints} object
*
* @param direction the direction to search in for the pattern
* @return a new @link SearchHints} object
*/
@NonNull
public static SearchHints create(@NonNull SearchDirection direction) {
return new SearchHints(direction);
}
/**
* Indicates that pattern matches should apply to whole words only
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchWholeWord() {
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for whole words only */
public boolean isWholeWord() {
return mWholeWord;
}
/**
* Indicates that pattern matches should apply to Java symbols only
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchJavaSymbol() {
mJavaSymbol = true;
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for Java symbols only */
public boolean isJavaSymbol() {
return mJavaSymbol;
}
/**
* Indicates that pattern matches should apply to constructors. If so, look for
* some other possible source aliases too, such as "super".
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchConstructor() {
mConstructor = true;
mWholeWord = true;
mJavaSymbol = true;
return this;
}
/** @return true if the pattern match should be for a constructor */
public boolean isConstructor() {
return mConstructor;
}
}
}
@@ -0,0 +1,50 @@
/*
* 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.detector.api;
import com.google.common.annotations.Beta;
/**
* Information about a position in a file/document.
* <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 abstract class Position {
/**
* Returns the line number (0-based where the first line is line 0)
*
* @return the 0-based line number
*/
public abstract int getLine();
/**
* The character offset
*
* @return the 0-based character offset
*/
public abstract int getOffset();
/**
* Returns the column number (where the first character on the line is 0),
* or -1 if unknown
*
* @return the 0-based column number
*/
public abstract int getColumn();
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
/*
* Copyright (C) 2014 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* A {@link com.android.tools.lint.detector.api.Context} used when checking resource files
* (both bitmaps and XML files; for XML files a subclass of this context is used:
* {@link com.android.tools.lint.detector.api.XmlContext}.)
* <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 ResourceContext extends Context {
private final ResourceFolderType mFolderType;
/**
* Construct a new {@link com.android.tools.lint.detector.api.ResourceContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link com.android.resources.ResourceFolderType} of this file, if any
*/
public ResourceContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType) {
super(driver, project, main, file);
mFolderType = folderType;
}
/**
* Returns the resource folder type of this XML file, if any.
*
* @return the resource folder type or null
*/
@Nullable
public ResourceFolderType getResourceFolderType() {
return mFolderType;
}
/**
* Returns the folder version. For example, for the file values-v14/foo.xml,
* it returns 14.
*
* @return the folder version, or -1 if no specific version was specified
*/
public int getFolderVersion() {
return mDriver.getResourceFolderVersion(file);
}
}
@@ -0,0 +1,61 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Specialized detector intended for XML resources. Detectors that apply to XML
* resources should extend this detector instead since it provides special
* iteration hooks that are more efficient.
* <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 abstract class ResourceXmlDetector extends Detector implements Detector.XmlScanner {
@Override
public boolean appliesTo(@NonNull Context context, @NonNull File file) {
return LintUtils.isXmlFile(file);
}
/**
* Returns whether this detector applies to the given folder type. This
* allows the detectors to be pruned from iteration, so for example when we
* are analyzing a string value file we don't need to look up detectors
* related to layout.
*
* @param folderType the folder type to be visited
* @return true if this detector can apply to resources in folders of the
* given type
*/
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return true;
}
@Override
public void run(@NonNull Context context) {
// The infrastructure should never call this method on an xml detector since
// it will run the various visitors instead
assert false;
}
}
@@ -0,0 +1,264 @@
/*
* 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.detector.api;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.DOT_CLASS;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_PNG;
import static com.android.SdkConstants.DOT_PROPERTIES;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE;
import static com.android.SdkConstants.OLD_PROGUARD_FILE;
import static com.android.SdkConstants.RES_FOLDER;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
/**
* The scope of a detector is the set of files a detector must consider when
* performing its analysis. This can be used to determine when issues are
* potentially obsolete, whether a detector should re-run on a file save, etc.
* <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 enum Scope {
/**
* The analysis only considers a single XML resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
RESOURCE_FILE,
/**
* The analysis only considers a single binary (typically a bitmap) resource file at a time.
* <p>
* Issues which are only affected by a single resource file can be checked
* for incrementally when a file is edited.
*/
BINARY_RESOURCE_FILE,
/**
* The analysis considers the resource folders
*/
RESOURCE_FOLDER,
/**
* The analysis considers <b>all</b> the resource file. This scope must not
* be used in conjunction with {@link #RESOURCE_FILE}; an issue scope is
* either considering just a single resource file or all the resources, not
* both.
*/
ALL_RESOURCE_FILES,
/**
* The analysis only considers a single Java source file at a time.
* <p>
* Issues which are only affected by a single Java source file can be
* checked for incrementally when a Java source file is edited.
*/
JAVA_FILE,
/**
* The analysis considers <b>all</b> the Java source files together.
* <p>
* This flag is mutually exclusive with {@link #JAVA_FILE}.
*/
ALL_JAVA_FILES,
/**
* The analysis only considers a single Java class file at a time.
* <p>
* Issues which are only affected by a single Java class file can be checked
* for incrementally when a Java source file is edited and then recompiled.
*/
CLASS_FILE,
/**
* The analysis considers <b>all</b> the Java class files together.
* <p>
* This flag is mutually exclusive with {@link #CLASS_FILE}.
*/
ALL_CLASS_FILES,
/** The analysis considers the manifest file */
MANIFEST,
/** The analysis considers the Proguard configuration file */
PROGUARD_FILE,
/**
* The analysis considers classes in the libraries for this project. These
* will be analyzed before the classes themselves.
*/
JAVA_LIBRARIES,
/** The analysis considers a Gradle build file */
GRADLE_FILE,
/** The analysis considers Java property files */
PROPERTY_FILE,
/** The analysis considers test sources as well */
TEST_SOURCES,
/**
* Scope for other files. Issues that specify a custom scope will be called unconditionally.
* This will call {@link Detector#run(Context)}} on the detectors unconditionally.
*/
OTHER;
/**
* Returns true if the given scope set corresponds to scanning a single file
* rather than a whole project
*
* @param scopes the scope set to check
* @return true if the scope set references a single file
*/
public static boolean checkSingleFile(@NonNull EnumSet<Scope> scopes) {
int size = scopes.size();
if (size == 2) {
// When single checking a Java source file, we check both its Java source
// and the associated class files
return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE);
} else {
return size == 1 &&
(scopes.contains(JAVA_FILE)
|| scopes.contains(CLASS_FILE)
|| scopes.contains(RESOURCE_FILE)
|| scopes.contains(PROGUARD_FILE)
|| scopes.contains(PROPERTY_FILE)
|| scopes.contains(GRADLE_FILE)
|| scopes.contains(MANIFEST));
}
}
/**
* Returns the intersection of two scope sets
*
* @param scope1 the first set to intersect
* @param scope2 the second set to intersect
* @return the intersection of the two sets
*/
@NonNull
public static EnumSet<Scope> intersect(
@NonNull EnumSet<Scope> scope1,
@NonNull EnumSet<Scope> scope2) {
EnumSet<Scope> scope = EnumSet.copyOf(scope1);
scope.retainAll(scope2);
return scope;
}
/**
* Infers a suitable scope to use from the given projects to be analyzed
* @param projects the projects to find a suitable scope for
* @return the scope to use
*/
@NonNull
public static EnumSet<Scope> infer(@NonNull Collection<Project> projects) {
// Infer the scope
EnumSet<Scope> scope = EnumSet.noneOf(Scope.class);
for (Project project : projects) {
List<File> subset = project.getSubset();
if (subset != null) {
for (File file : subset) {
String name = file.getName();
if (name.equals(ANDROID_MANIFEST_XML)) {
scope.add(MANIFEST);
} else if (name.endsWith(DOT_XML)) {
scope.add(RESOURCE_FILE);
} else if (name.endsWith(DOT_JAVA)) {
scope.add(JAVA_FILE);
} else if (name.endsWith(DOT_CLASS)) {
scope.add(CLASS_FILE);
} else if (name.endsWith(DOT_GRADLE)) {
scope.add(GRADLE_FILE);
} else if (name.equals(OLD_PROGUARD_FILE)
|| name.equals(FN_PROJECT_PROGUARD_FILE)) {
scope.add(PROGUARD_FILE);
} else if (name.endsWith(DOT_PROPERTIES)) {
scope.add(PROPERTY_FILE);
} else if (name.endsWith(DOT_PNG)) {
scope.add(BINARY_RESOURCE_FILE);
} else if (name.equals(RES_FOLDER)
|| file.getParent().equals(RES_FOLDER)) {
scope.add(ALL_RESOURCE_FILES);
scope.add(RESOURCE_FILE);
scope.add(BINARY_RESOURCE_FILE);
scope.add(RESOURCE_FOLDER);
}
}
} else {
// Specified a full project: just use the full project scope
scope = Scope.ALL;
break;
}
}
return scope;
}
/** All scopes: running lint on a project will check these scopes */
public static final EnumSet<Scope> ALL = EnumSet.allOf(Scope.class);
/** Scope-set used for detectors which are affected by a single resource file */
public static final EnumSet<Scope> RESOURCE_FILE_SCOPE = EnumSet.of(RESOURCE_FILE);
/** Scope-set used for detectors which are affected by a single resource folder */
public static final EnumSet<Scope> RESOURCE_FOLDER_SCOPE = EnumSet.of(RESOURCE_FOLDER);
/** Scope-set used for detectors which scan all resources */
public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES);
/** Scope-set used for detectors which are affected by a single Java source file */
public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE);
/** Scope-set used for detectors which are affected by a single Java class file */
public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE);
/** Scope-set used for detectors which are affected by a single Gradle build file */
public static final EnumSet<Scope> GRADLE_SCOPE = EnumSet.of(GRADLE_FILE);
/** Scope-set used for detectors which are affected by the manifest only */
public static final EnumSet<Scope> MANIFEST_SCOPE = EnumSet.of(MANIFEST);
/** Scope-set used for detectors which correspond to some other context */
public static final EnumSet<Scope> OTHER_SCOPE = EnumSet.of(OTHER);
/** Scope-set used for detectors which are affected by a single ProGuard class file */
public static final EnumSet<Scope> PROGUARD_SCOPE = EnumSet.of(PROGUARD_FILE);
/** Scope-set used for detectors which correspond to property files */
public static final EnumSet<Scope> PROPERTY_SCOPE = EnumSet.of(PROPERTY_FILE);
/** Resource XML files and manifest files */
public static final EnumSet<Scope> MANIFEST_AND_RESOURCE_SCOPE =
EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE);
/** Scope-set used for detectors which are affected by single XML and Java source files */
public static final EnumSet<Scope> JAVA_AND_RESOURCE_FILES =
EnumSet.of(RESOURCE_FILE, JAVA_FILE);
/** Scope-set used for analyzing individual class files and all resource files */
public static final EnumSet<Scope> CLASS_AND_ALL_RESOURCE_FILES =
EnumSet.of(ALL_RESOURCE_FILES, CLASS_FILE);
/** Scope-set used for analyzing all class files, including those in libraries */
public static final EnumSet<Scope> ALL_CLASSES_AND_LIBRARIES =
EnumSet.of(Scope.ALL_CLASS_FILES, Scope.JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by Java libraries */
public static final EnumSet<Scope> JAVA_LIBRARY_SCOPE = EnumSet.of(JAVA_LIBRARIES);
/** Scope-set used for detectors which are affected by a single binary resource file */
public static final EnumSet<Scope> BINARY_RESOURCE_FILE_SCOPE =
EnumSet.of(BINARY_RESOURCE_FILE);
}
@@ -0,0 +1,103 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.google.common.annotations.Beta;
/**
* Severity of an issue found by lint
* <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 enum Severity {
/**
* Fatal: Use sparingly because a warning marked as fatal will be
* considered critical and will abort Export APK etc in ADT
*/
@NonNull
FATAL("Fatal"),
/**
* Errors: The issue is known to be a real error that must be addressed.
*/
@NonNull
ERROR("Error"),
/**
* Warning: Probably a problem.
*/
@NonNull
WARNING("Warning"),
/**
* Information only: Might not be a problem, but the check has found
* something interesting to say about the code.
*/
@NonNull
INFORMATIONAL("Information"),
/**
* Ignore: The user doesn't want to see this issue
*/
@NonNull
IGNORE("Ignore");
@NonNull
private final String mDisplay;
Severity(@NonNull String display) {
mDisplay = display;
}
/**
* Returns a description of this severity suitable for display to the user
*
* @return a description of the severity
*/
@NonNull
public String getDescription() {
return mDisplay;
}
/** Returns the name of this severity */
@NonNull
public String getName() {
return name();
}
/**
* Looks up the severity corresponding to a given named severity. The severity
* string should be one returned by {@link #toString()}
*
* @param name the name to look up
* @return the corresponding severity, or null if it is not a valid severity name
*/
@Nullable
public static Severity fromName(@NonNull String name) {
for (Severity severity : values()) {
if (severity.name().equalsIgnoreCase(name)) {
return severity;
}
}
return null;
}
}
@@ -0,0 +1,58 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.google.common.annotations.Beta;
/**
* Enum which describes the different computation speeds of various detectors
* <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 enum Speed {
/** The detector can run very quickly */
FAST("Fast"),
/** The detector runs reasonably fast */
NORMAL("Normal"),
/** The detector might take a long time to run */
SLOW("Slow"),
/** The detector might take a huge amount of time to run */
REALLY_SLOW("Really Slow");
private final String mDisplayName;
Speed(@NonNull String displayName) {
mDisplayName = displayName;
}
/**
* Returns the user-visible description of the speed of the given
* detector
*
* @return the description of the speed to display to the user
*/
@NonNull
public String getDisplayName() {
return mDisplayName;
}
}
@@ -0,0 +1,300 @@
/*
* Copyright (C) 2014 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.detector.api;
import com.android.annotations.NonNull;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
/**
* Lint error message, issue explanations and location descriptions
* are described in a {@link #RAW} format which looks similar to text
* but which can contain bold, symbols and links. These issues can
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getDescription(TextFormat)
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
public enum TextFormat {
/**
* Raw output format which is similar to text but allows some markup:
* <ul>
* <li>HTTP urls (http://...)
* <li>Sentences immediately surrounded by * will be shown as bold.
* <li>Sentences immediately surrounded by ` will be shown using monospace
* fonts
* </ul>
* Furthermore, newlines are converted to br's when converting newlines.
* Note: It does not insert {@code <html>} tags around the fragment for HTML output.
* <p>
* TODO: Consider switching to the restructured text format -
* http://docutils.sourceforge.net/docs/user/rst/quickstart.html
*/
RAW,
/**
* Plain text output
*/
TEXT,
/**
* HTML formatted output (note: does not include surrounding {@code <html></html>} tags)
*/
HTML;
/**
* Converts the given text to HTML
*
* @param text the text to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toHtml(@NonNull String text) {
return convertTo(text, HTML);
}
/**
* Converts the given text to plain text
*
* @param text the tetx to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toText(@NonNull String text) {
return convertTo(text, TEXT);
}
/**
* Converts the given message to the given format. Note that some
* conversions are lossy; e.g. once converting away from the raw format
* (which contains all the markup) you can't convert back to it.
* Note that you can convert to the format it's already in; that just
* returns the same string.
*
* @param message the message to convert
* @param to the format to convert to
* @return a converted message
*/
public String convertTo(@NonNull String message, @NonNull TextFormat to) {
if (this == to) {
return message;
}
switch (this) {
case RAW: {
switch (to) {
case RAW:
return message;
case TEXT:
case HTML:
return to.fromRaw(message);
}
}
case TEXT: {
switch (to) {
case TEXT:
case RAW:
return message;
case HTML:
return XmlUtils.toXmlTextValue(message);
}
}
case HTML: {
switch (to) {
case HTML:
return message;
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
}
return message;
}
/** Converts to this output format from the given HTML-format text */
@NonNull
private String fromHtml(@NonNull String html) {
assert this == RAW || this == TEXT : this;
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Scan forward to the end
if (html.startsWith("<br>", i) ||
html.startsWith("<br />", i) ||
html.startsWith("<BR>", i) ||
html.startsWith("<BR />", i)) {
sb.append('\n');
} else if (html.startsWith("<!--")) {
i = Math.max(i, html.indexOf("-->", i));
}
i = html.indexOf('>', i);
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
sb.append(XmlUtils.fromXmlAttributeValue(entity));
i = end;
} else {
sb.append(c);
}
} else if (c == '\n') {
sb.append(' ');
} else {
sb.append(c);
}
}
// Collapse repeated spaces
String s = sb.toString();
sb.setLength(0);
boolean wasSpace = false;
for (int i = 0, n = s.length(); i < n; i++) {
char c = s.charAt(i);
if (c == '\t') { // we keep newlines; came from <br>'s
c = ' ';
}
boolean isSpace = c == ' ';
if (!isSpace || !wasSpace) {
wasSpace = isSpace;
sb.append(c);
}
}
s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
return s;
}
private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
boolean html = this == HTML;
char prev = 0;
int flushIndex = 0;
int n = text.length();
for (int i = 0; i < n; i++) {
char c = text.charAt(i);
if ((c == '*' || c == '`' && i < n - 1)) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
// Found * or ` immediately before a letter, and not in the middle of a word
// Find end
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
}
if (html) {
String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$
sb.append('<').append(tag).append('>');
appendEscapedText(sb, text, html, i + 1, end);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
} else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't'
&& text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) {
// Find url end
int end = i + HTTP_PREFIX.length();
while (end < n) {
char d = text.charAt(end);
if (Character.isWhitespace(d)) {
break;
}
end++;
}
char last = text.charAt(end - 1);
if (last == '.' || last == ')' || last == '!') {
end--;
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i);
}
String url = text.substring(i, end);
sb.append("<a href=\""); //$NON-NLS-1$
sb.append(url);
sb.append('"').append('>');
sb.append(url);
sb.append("</a>"); //$NON-NLS-1$
flushIndex = end;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
prev = c;
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n);
}
return sb.toString();
}
private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text,
boolean html, int start, int end) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c == '<') {
sb.append("&lt;"); //$NON-NLS-1$
} else if (c == '&') {
sb.append("&amp;"); //$NON-NLS-1$
} else if (c == '\n') {
sb.append("<br/>\n");
} else {
if (c > 255) {
sb.append("&#"); //$NON-NLS-1$
sb.append(Integer.toString(c));
sb.append(';');
} else if (c == '\u00a0') {
sb.append("&nbsp;"); //$NON-NLS-1$
} else {
sb.append(c);
}
}
}
} else {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
sb.append(c);
}
}
}
}
@@ -0,0 +1,205 @@
/*
* 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.detector.api;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.XmlParser;
import com.google.common.annotations.Beta;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.io.File;
/**
* A {@link Context} used when checking XML files.
* <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 XmlContext extends ResourceContext {
static final String SUPPRESS_COMMENT_PREFIX = "<!--suppress "; //$NON-NLS-1$
/** The XML parser */
private final XmlParser mParser;
/** The XML document */
public Document document;
/**
* Construct a new {@link XmlContext}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
* @param folderType the {@link ResourceFolderType} of this file, if any
*/
public XmlContext(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file,
@Nullable ResourceFolderType folderType,
@NonNull XmlParser parser) {
super(driver, project, main, file, folderType);
mParser = parser;
}
/**
* Returns the location for the given node, which may be an element or an attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getLocation(@NonNull Node node) {
return mParser.getLocation(this, node);
}
/**
* Returns the location for name-portion of the given element or attribute.
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getNameLocation(@NonNull Node node) {
return mParser.getNameLocation(this, node);
}
/**
* Returns the location for value-portion of the given attribute
*
* @param node the node to look up the location for
* @return the location for the node
*/
@NonNull
public Location getValueLocation(@NonNull Attr node) {
return mParser.getValueLocation(this, node);
}
/**
* Creates a new location within an XML text node
*
* @param textNode the text node
* @param begin the start offset within the text node (inclusive)
* @param end the end offset within the text node (exclusive)
* @return a new location
*/
@NonNull
public Location getLocation(@NonNull Node textNode, int begin, int end) {
assert textNode.getNodeType() == Node.TEXT_NODE;
return mParser.getLocation(this, textNode, begin, end);
}
@NonNull
public XmlParser getParser() {
return mParser;
}
/**
* Reports an issue applicable to a given DOM node. The DOM node is used as the
* scope to check for suppress lint annotations.
*
* @param issue the issue to report
* @param scope the DOM node scope the error applies to. The lint infrastructure
* will check whether there are suppress directives on this node (or its enclosing
* nodes) and if so suppress the warning without involving the client.
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message) {
if (scope != null && mDriver.isSuppressed(this, issue, scope)) {
return;
}
super.report(issue, location, message);
}
/**
* Report an error.
* Like {@link #report(Issue, org.w3c.dom.Node, Location, String)} but with
* a now-unused data parameter at the end.
*
* @deprecated Use {@link #report(Issue, org.w3c.dom.Node, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Node scope,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, scope, location, message);
}
@Override
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
// Warn if clients use the non-scoped form? No, there are cases where an
// XML detector's error isn't applicable to one particular location (or it's
// not feasible to compute it cheaply)
//mDriver.getClient().log(null, "Warning: Issue " + issue
// + " was reported without a scope node: Can't be suppressed.");
// For now just check the document root itself
if (document != null && mDriver.isSuppressed(this, issue, document)) {
return;
}
super.report(issue, location, message);
}
@Override
@Nullable
protected String getSuppressCommentPrefix() {
return SUPPRESS_COMMENT_PREFIX;
}
public boolean isSuppressedWithComment(@NonNull Node node, @NonNull Issue issue) {
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
int start = mParser.getNodeStartOffset(this, node);
if (start != -1) {
return isSuppressedWithComment(start, issue);
}
return false;
}
@NonNull
public Location.Handle createLocationHandle(@NonNull Node node) {
return mParser.createLocationHandle(this, node);
}
}