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