• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.lint.detector.api;
18 
19 import static com.android.tools.lint.detector.api.LintConstants.DOT_CLASS;
20 import static com.android.tools.lint.detector.api.LintConstants.DOT_JAVA;
21 
22 import com.android.annotations.NonNull;
23 import com.android.annotations.Nullable;
24 import com.android.tools.lint.client.api.LintDriver;
25 import com.google.common.annotations.Beta;
26 
27 import org.objectweb.asm.Type;
28 import org.objectweb.asm.tree.AbstractInsnNode;
29 import org.objectweb.asm.tree.ClassNode;
30 import org.objectweb.asm.tree.FieldNode;
31 import org.objectweb.asm.tree.LineNumberNode;
32 import org.objectweb.asm.tree.MethodNode;
33 
34 import java.io.File;
35 import java.util.List;
36 
37 /**
38  * A {@link Context} used when checking .class files.
39  * <p/>
40  * <b>NOTE: This is not a public or final API; if you rely on this be prepared
41  * to adjust your code for the next tools release.</b>
42  */
43 @Beta
44 public class ClassContext extends Context {
45     private final File mBinDir;
46     /** The class file DOM root node */
47     private ClassNode mClassNode;
48     /** The class file byte data */
49     private byte[] mBytes;
50     /** The source file, if known/found */
51     private File mSourceFile;
52     /** The contents of the source file, if source file is known/found */
53     private String mSourceContents;
54     /** Whether we've searched for the source file (used to avoid repeated failed searches) */
55     private boolean mSearchedForSource;
56     /** If the file is a relative path within a jar file, this is the jar file, otherwise null */
57     private final File mJarFile;
58     /** Whether this class is part of a library (rather than corresponding to one of the
59      * source files in this project */
60     private final boolean mFromLibrary;
61 
62     /**
63      * Construct a new {@link ClassContext}
64      *
65      * @param driver the driver running through the checks
66      * @param project the project containing the file being checked
67      * @param main the main project if this project is a library project, or
68      *            null if this is not a library project. The main project is the
69      *            root project of all library projects, not necessarily the
70      *            directly including project.
71      * @param file the file being checked
72      * @param jarFile If the file is a relative path within a jar file, this is
73      *            the jar file, otherwise null
74      * @param binDir the root binary directory containing this .class file.
75      * @param bytes the bytecode raw data
76      * @param classNode the bytecode object model
77      * @param fromLibrary whether this class is from a library rather than part
78      *            of this project
79      */
ClassContext( @onNull 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)80     public ClassContext(
81             @NonNull LintDriver driver,
82             @NonNull Project project,
83             @Nullable Project main,
84             @NonNull File file,
85             @Nullable File jarFile,
86             @NonNull File binDir,
87             @NonNull byte[] bytes,
88             @NonNull ClassNode classNode,
89             boolean fromLibrary) {
90         super(driver, project, main, file);
91         mJarFile = jarFile;
92         mBinDir = binDir;
93         mBytes = bytes;
94         mClassNode = classNode;
95         mFromLibrary = fromLibrary;
96     }
97 
98     /**
99      * Returns the raw bytecode data for this class file
100      *
101      * @return the byte array containing the bytecode data
102      */
103     @NonNull
getBytecode()104     public byte[] getBytecode() {
105         return mBytes;
106     }
107 
108     /**
109      * Returns the bytecode object model
110      *
111      * @return the bytecode object model, never null
112      */
113     @NonNull
getClassNode()114     public ClassNode getClassNode() {
115         return mClassNode;
116     }
117 
118     /**
119      * Returns the jar file, if any. If this is null, the .class file is a real file
120      * on disk, otherwise it represents a relative path within the jar file.
121      *
122      * @return the jar file, or null
123      */
124     @Nullable
getJarFile()125     public File getJarFile() {
126         return mJarFile;
127     }
128 
129     /**
130      * Returns whether this class is part of a library (not this project).
131      *
132      * @return true if this class is part of a library
133      */
isFromClassLibrary()134     public boolean isFromClassLibrary() {
135         return mFromLibrary;
136     }
137 
138     /**
139      * Returns the source file for this class file, if possible.
140      *
141      * @return the source file, or null
142      */
143     @Nullable
getSourceFile()144     public File getSourceFile() {
145         if (mSourceFile == null && !mSearchedForSource) {
146             mSearchedForSource = true;
147 
148             String source = mClassNode.sourceFile;
149             if (source == null) {
150                 source = file.getName();
151                 if (source.endsWith(DOT_CLASS)) {
152                     source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA;
153                 }
154                 int index = source.indexOf('$');
155                 if (index != -1) {
156                     source = source.substring(0, index) + DOT_JAVA;
157                 }
158             }
159             if (source != null) {
160                 if (mJarFile != null) {
161                     String relative = file.getParent() + File.separator + source;
162                     List<File> sources = getProject().getJavaSourceFolders();
163                     for (File dir : sources) {
164                         File sourceFile = new File(dir, relative);
165                         if (sourceFile.exists()) {
166                             mSourceFile = sourceFile;
167                             break;
168                         }
169                     }
170                 } else {
171                     // Determine package
172                     String topPath = mBinDir.getPath();
173                     String parentPath = file.getParentFile().getPath();
174                     if (parentPath.startsWith(topPath)) {
175                         String relative = parentPath.substring(topPath.length() + 1);
176                         List<File> sources = getProject().getJavaSourceFolders();
177                         for (File dir : sources) {
178                             File sourceFile = new File(dir, relative + File.separator + source);
179                             if (sourceFile.exists()) {
180                                 mSourceFile = sourceFile;
181                                 break;
182                             }
183                         }
184                     }
185                 }
186             }
187         }
188 
189         return mSourceFile;
190     }
191 
192     /**
193      * Returns the contents of the source file for this class file, if found.
194      *
195      * @return the source contents, or ""
196      */
197     @NonNull
getSourceContents()198     public String getSourceContents() {
199         if (mSourceContents == null) {
200             File sourceFile = getSourceFile();
201             if (sourceFile != null) {
202                 mSourceContents = getClient().readFile(mSourceFile);
203             }
204 
205             if (mSourceContents == null) {
206                 mSourceContents = "";
207             }
208         }
209 
210         return mSourceContents;
211     }
212 
213     /**
214      * Returns a location for the given source line number in this class file's
215      * source file, if available.
216      *
217      * @param line the line number (1-based, which is what ASM uses)
218      * @param patternStart optional pattern to search for in the source for
219      *            range start
220      * @param patternEnd optional pattern to search for in the source for range
221      *            end
222      * @return a location, never null
223      */
224     @NonNull
getLocationForLine(int line, String patternStart, String patternEnd)225     public Location getLocationForLine(int line, String patternStart, String patternEnd) {
226         File sourceFile = getSourceFile();
227         if (sourceFile != null) {
228             // ASM line numbers are 1-based, and lint line numbers are 0-based
229             if (line != -1) {
230                 return Location.create(sourceFile, getSourceContents(), line - 1,
231                         patternStart, patternEnd);
232             } else {
233                 return Location.create(sourceFile);
234             }
235         }
236 
237         return Location.create(file);
238     }
239 
240     /**
241      * Reports an issue.
242      * <p>
243      * Detectors should only call this method if an error applies to the whole class
244      * scope and there is no specific method or field that applies to the error.
245      * If so, use
246      * {@link #report(Issue, MethodNode, Location, String, Object)} or
247      * {@link #report(Issue, FieldNode, Location, String, Object)}, such that
248      * suppress annotations are checked.
249      *
250      * @param issue the issue to report
251      * @param location the location of the issue, or null if not known
252      * @param message the message for this warning
253      * @param data any associated data, or null
254      */
255     @Override
report(Issue issue, Location location, String message, Object data)256     public void report(Issue issue, Location location, String message, Object data) {
257         if (mDriver.isSuppressed(issue, mClassNode)) {
258             return;
259         }
260         ClassNode curr = mClassNode;
261         while (curr != null) {
262             ClassNode prev = curr;
263             curr = mDriver.getOuterClassNode(curr);
264             if (curr != null) {
265                 if (prev.outerMethod != null) {
266                     @SuppressWarnings("rawtypes") // ASM API
267                     List methods = curr.methods;
268                     for (Object m : methods) {
269                         MethodNode method = (MethodNode) m;
270                         if (method.name.equals(prev.outerMethod)
271                                 && method.desc.equals(prev.outerMethodDesc)) {
272                             // Found the outer method for this anonymous class; continue
273                             // reporting on it (which will also work its way up the parent
274                             // class hierarchy)
275                             if (method != null && mDriver.isSuppressed(issue, method)) {
276                                 return;
277                             }
278                             break;
279                         }
280                     }
281                 }
282                 if (mDriver.isSuppressed(issue, curr)) {
283                     return;
284                 }
285             }
286         }
287 
288         super.report(issue, location, message, data);
289     }
290 
291     // Unfortunately, ASMs nodes do not extend a common DOM node type with parent
292     // pointers, so we have to have multiple methods which pass in each type
293     // of node (class, method, field) to be checked.
294 
295     /**
296      * Reports an issue applicable to a given method node.
297      *
298      * @param issue the issue to report
299      * @param method the method scope the error applies to. The lint infrastructure
300      *    will check whether there are suppress annotations on this method (or its enclosing
301      *    class) and if so suppress the warning without involving the client.
302      * @param location the location of the issue, or null if not known
303      * @param message the message for this warning
304      * @param data any associated data, or null
305      */
report( @onNull Issue issue, @Nullable MethodNode method, @Nullable Location location, @NonNull String message, @Nullable Object data)306     public void report(
307             @NonNull Issue issue,
308             @Nullable MethodNode method,
309             @Nullable Location location,
310             @NonNull String message,
311             @Nullable Object data) {
312         if (method != null && mDriver.isSuppressed(issue, method)) {
313             return;
314         }
315         report(issue, location, message, data); // also checks the class node
316     }
317 
318     /**
319      * Reports an issue applicable to a given method node.
320      *
321      * @param issue the issue to report
322      * @param field the scope the error applies to. The lint infrastructure
323      *    will check whether there are suppress annotations on this field (or its enclosing
324      *    class) and if so suppress the warning without involving the client.
325      * @param location the location of the issue, or null if not known
326      * @param message the message for this warning
327      * @param data any associated data, or null
328      */
report( @onNull Issue issue, @Nullable FieldNode field, @Nullable Location location, @NonNull String message, @Nullable Object data)329     public void report(
330             @NonNull Issue issue,
331             @Nullable FieldNode field,
332             @Nullable Location location,
333             @NonNull String message,
334             @Nullable Object data) {
335         if (field != null && mDriver.isSuppressed(issue, field)) {
336             return;
337         }
338         report(issue, location, message, data); // also checks the class node
339     }
340 
341     /**
342      * Finds the line number closest to the given node
343      *
344      * @param node the instruction node to get a line number for
345      * @return the closest line number, or -1 if not known
346      */
findLineNumber(AbstractInsnNode node)347     public static int findLineNumber(AbstractInsnNode node) {
348         AbstractInsnNode curr = node;
349 
350         // First search backwards
351         while (curr != null) {
352             if (curr.getType() == AbstractInsnNode.LINE) {
353                 return ((LineNumberNode) curr).line;
354             }
355             curr = curr.getPrevious();
356         }
357 
358         // Then search forwards
359         curr = node;
360         while (curr != null) {
361             if (curr.getType() == AbstractInsnNode.LINE) {
362                 return ((LineNumberNode) curr).line;
363             }
364             curr = curr.getNext();
365         }
366 
367         return -1;
368     }
369 
370     /**
371      * Finds the line number closest to the given method declaration
372      *
373      * @param node the method node to get a line number for
374      * @return the closest line number, or -1 if not known
375      */
findLineNumber(MethodNode node)376     public static int findLineNumber(MethodNode node) {
377         if (node.instructions != null && node.instructions.size() > 0) {
378             return findLineNumber(node.instructions.get(0));
379         }
380 
381         return -1;
382     }
383 
384     /**
385      * Computes a user-readable type signature from the given class owner, name
386      * and description. For example, for owner="foo/bar/Foo$Baz", name="foo",
387      * description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)".
388      *
389      * @param owner the class name
390      * @param name the method name
391      * @param desc the method description
392      * @return a user-readable string
393      */
createSignature(String owner, String name, String desc)394     public static String createSignature(String owner, String name, String desc) {
395         StringBuilder sb = new StringBuilder();
396 
397         if (desc != null) {
398             Type returnType = Type.getReturnType(desc);
399             sb.append(getTypeString(returnType));
400             sb.append(' ');
401         }
402 
403         if (owner != null) {
404             sb.append(owner.replace('/', '.').replace('$','.'));
405         }
406         if (name != null) {
407             sb.append('#');
408             sb.append(name);
409             if (desc != null) {
410                 Type[] argumentTypes = Type.getArgumentTypes(desc);
411                 if (argumentTypes != null && argumentTypes.length > 0) {
412                     sb.append('(');
413                     boolean first = true;
414                     for (Type type : argumentTypes) {
415                         if (first) {
416                             first = false;
417                         } else {
418                             sb.append(", ");
419                         }
420                         sb.append(getTypeString(type));
421                     }
422                     sb.append(')');
423                 }
424             }
425         }
426 
427         return sb.toString();
428     }
429 
getTypeString(Type type)430     private static String getTypeString(Type type) {
431         String s = type.getClassName();
432         if (s.startsWith("java.lang.")) {           //$NON-NLS-1$
433             s = s.substring("java.lang.".length()); //$NON-NLS-1$
434         }
435 
436         return s;
437     }
438 
439     /**
440      * Computes the internal class name of the given fully qualified class name.
441      * For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar
442      *
443      * @param fqcn the fully qualified class name
444      * @return the internal class name
445      */
getInternalName(String fqcn)446     public static String getInternalName(String fqcn) {
447         String[] parts = fqcn.split("\\."); //$NON-NLS-1$
448         StringBuilder sb = new StringBuilder();
449         String prev = null;
450         for (String part : parts) {
451             if (prev != null) {
452                 if (Character.isUpperCase(prev.charAt(0))) {
453                     sb.append('$');
454                 } else {
455                     sb.append('/');
456                 }
457             }
458             sb.append(part);
459             prev = part;
460         }
461 
462         return sb.toString();
463     }
464 }
465