blob: 2d08ed99380027e9e521142ffcbfbb676031ae98 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.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();
}
}