| /* |
| * 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(); |
| } |
| } |