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.ANDROID_LIBRARY; 20 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_LIBRARY_REFERENCE_FORMAT; 21 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_MANIFEST_XML; 22 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI; 23 import static com.android.tools.lint.detector.api.LintConstants.ATTR_MIN_SDK_VERSION; 24 import static com.android.tools.lint.detector.api.LintConstants.ATTR_PACKAGE; 25 import static com.android.tools.lint.detector.api.LintConstants.ATTR_TARGET_SDK_VERSION; 26 import static com.android.tools.lint.detector.api.LintConstants.PROGUARD_CONFIG; 27 import static com.android.tools.lint.detector.api.LintConstants.PROJECT_PROPERTIES; 28 import static com.android.tools.lint.detector.api.LintConstants.TAG_USES_SDK; 29 import static com.android.tools.lint.detector.api.LintConstants.VALUE_TRUE; 30 31 import com.android.annotations.NonNull; 32 import com.android.annotations.Nullable; 33 import com.android.tools.lint.client.api.Configuration; 34 import com.android.tools.lint.client.api.LintClient; 35 import com.android.tools.lint.client.api.SdkInfo; 36 import com.google.common.annotations.Beta; 37 import com.google.common.base.Charsets; 38 import com.google.common.io.Closeables; 39 import com.google.common.io.Files; 40 41 import org.w3c.dom.Document; 42 import org.w3c.dom.Element; 43 import org.w3c.dom.NodeList; 44 45 import java.io.BufferedInputStream; 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.IOException; 49 import java.util.ArrayList; 50 import java.util.Collection; 51 import java.util.Collections; 52 import java.util.List; 53 import java.util.Properties; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 57 /** 58 * A project contains information about an Android project being scanned for 59 * Lint errors. 60 * <p> 61 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 62 * to adjust your code for the next tools release.</b> 63 */ 64 @Beta 65 public class Project { 66 private final LintClient mClient; 67 private final File mDir; 68 private final File mReferenceDir; 69 private Configuration mConfiguration; 70 private String mPackage; 71 private int mMinSdk = 1; 72 private int mTargetSdk = -1; 73 private boolean mLibrary; 74 private String mName; 75 private String mProguardPath; 76 77 /** The SDK info, if any */ 78 private SdkInfo mSdkInfo; 79 80 /** 81 * If non null, specifies a non-empty list of specific files under this 82 * project which should be checked. 83 */ 84 private List<File> mFiles; 85 private List<File> mJavaSourceFolders; 86 private List<File> mJavaClassFolders; 87 private List<File> mJavaLibraries; 88 private List<Project> mDirectLibraries; 89 private List<Project> mAllLibraries; 90 91 /** 92 * Creates a new {@link Project} for the given directory. 93 * 94 * @param client the tool running the lint check 95 * @param dir the root directory of the project 96 * @param referenceDir See {@link #getReferenceDir()}. 97 * @return a new {@link Project} 98 */ 99 @NonNull create( @onNull LintClient client, @NonNull File dir, @NonNull File referenceDir)100 public static Project create( 101 @NonNull LintClient client, 102 @NonNull File dir, 103 @NonNull File referenceDir) { 104 return new Project(client, dir, referenceDir); 105 } 106 107 /** Creates a new Project. Use one of the factory methods to create. */ Project( @onNull LintClient client, @NonNull File dir, @NonNull File referenceDir)108 private Project( 109 @NonNull LintClient client, 110 @NonNull File dir, 111 @NonNull File referenceDir) { 112 mClient = client; 113 mDir = dir; 114 mReferenceDir = referenceDir; 115 116 try { 117 // Read properties file and initialize library state 118 Properties properties = new Properties(); 119 File propFile = new File(dir, PROJECT_PROPERTIES); 120 if (propFile.exists()) { 121 BufferedInputStream is = new BufferedInputStream(new FileInputStream(propFile)); 122 try { 123 properties.load(is); 124 String value = properties.getProperty(ANDROID_LIBRARY); 125 mLibrary = VALUE_TRUE.equals(value); 126 mProguardPath = properties.getProperty(PROGUARD_CONFIG); 127 128 for (int i = 1; i < 1000; i++) { 129 String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i); 130 String library = properties.getProperty(key); 131 if (library == null || library.length() == 0) { 132 // No holes in the numbering sequence is allowed 133 break; 134 } 135 136 File libraryDir = new File(dir, library).getCanonicalFile(); 137 138 if (mDirectLibraries == null) { 139 mDirectLibraries = new ArrayList<Project>(); 140 } 141 142 // Adjust the reference dir to be a proper prefix path of the 143 // library dir 144 File libraryReferenceDir = referenceDir; 145 if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { 146 // Symlinks etc might have been resolved, so do those to 147 // the reference dir as well 148 libraryReferenceDir = libraryReferenceDir.getCanonicalFile(); 149 if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { 150 File f = libraryReferenceDir; 151 while (f != null && f.getPath().length() > 0) { 152 if (libraryDir.getPath().startsWith(f.getPath())) { 153 libraryReferenceDir = f; 154 break; 155 } 156 f = f.getParentFile(); 157 } 158 } 159 } 160 161 mDirectLibraries.add(client.getProject(libraryDir, libraryReferenceDir)); 162 } 163 } finally { 164 Closeables.closeQuietly(is); 165 } 166 } 167 } catch (IOException ioe) { 168 client.log(ioe, "Initializing project state"); 169 } 170 171 if (mDirectLibraries != null) { 172 mDirectLibraries = Collections.unmodifiableList(mDirectLibraries); 173 } else { 174 mDirectLibraries = Collections.emptyList(); 175 } 176 } 177 178 @Override toString()179 public String toString() { 180 return "Project [dir=" + mDir + "]"; 181 } 182 183 @Override hashCode()184 public int hashCode() { 185 final int prime = 31; 186 int result = 1; 187 result = prime * result + ((mDir == null) ? 0 : mDir.hashCode()); 188 return result; 189 } 190 191 @Override equals(@ullable Object obj)192 public boolean equals(@Nullable Object obj) { 193 if (this == obj) 194 return true; 195 if (obj == null) 196 return false; 197 if (getClass() != obj.getClass()) 198 return false; 199 Project other = (Project) obj; 200 if (mDir == null) { 201 if (other.mDir != null) 202 return false; 203 } else if (!mDir.equals(other.mDir)) 204 return false; 205 return true; 206 } 207 208 /** 209 * Adds the given file to the list of files which should be checked in this 210 * project. If no files are added, the whole project will be checked. 211 * 212 * @param file the file to be checked 213 */ addFile(@onNull File file)214 public void addFile(@NonNull File file) { 215 if (mFiles == null) { 216 mFiles = new ArrayList<File>(); 217 } 218 mFiles.add(file); 219 } 220 221 /** 222 * The list of files to be checked in this project. If null, the whole 223 * project should be checked. 224 * 225 * @return the subset of files to be checked, or null for the whole project 226 */ 227 @Nullable getSubset()228 public List<File> getSubset() { 229 return mFiles; 230 } 231 232 /** 233 * Returns the list of source folders for Java source files 234 * 235 * @return a list of source folders to search for .java files 236 */ 237 @NonNull getJavaSourceFolders()238 public List<File> getJavaSourceFolders() { 239 if (mJavaSourceFolders == null) { 240 if (isAospBuildEnvironment()) { 241 String top = getAospTop(); 242 if (mDir.getAbsolutePath().startsWith(top)) { 243 mJavaSourceFolders = getAospJavaSourcePath(); 244 return mJavaSourceFolders; 245 } 246 } 247 248 mJavaSourceFolders = mClient.getJavaSourceFolders(this); 249 } 250 251 return mJavaSourceFolders; 252 } 253 254 /** 255 * Returns the list of output folders for class files 256 * @return a list of output folders to search for .class files 257 */ 258 @NonNull getJavaClassFolders()259 public List<File> getJavaClassFolders() { 260 if (mJavaClassFolders == null) { 261 if (isAospBuildEnvironment()) { 262 String top = getAospTop(); 263 if (mDir.getAbsolutePath().startsWith(top)) { 264 mJavaClassFolders = getAospJavaClassPath(); 265 return mJavaClassFolders; 266 } 267 } 268 269 mJavaClassFolders = mClient.getJavaClassFolders(this); 270 } 271 return mJavaClassFolders; 272 } 273 274 /** 275 * Returns the list of Java libraries (typically .jar files) that this 276 * project depends on. Note that this refers to jar libraries, not Android 277 * library projects which are processed in a separate pass with their own 278 * source and class folders. 279 * 280 * @return a list of .jar files (or class folders) that this project depends 281 * on. 282 */ 283 @NonNull getJavaLibraries()284 public List<File> getJavaLibraries() { 285 if (mJavaLibraries == null) { 286 // AOSP builds already merge libraries and class folders into 287 // the single classes.jar file, so these have already been processed 288 // in getJavaClassFolders. 289 290 mJavaLibraries = mClient.getJavaLibraries(this); 291 } 292 293 return mJavaLibraries; 294 } 295 296 /** 297 * Returns the relative path of a given file relative to the user specified 298 * directory (which is often the project directory but sometimes a higher up 299 * directory when a directory tree is being scanned 300 * 301 * @param file the file under this project to check 302 * @return the path relative to the reference directory (often the project directory) 303 */ 304 @NonNull getDisplayPath(@onNull File file)305 public String getDisplayPath(@NonNull File file) { 306 String path = file.getPath(); 307 String referencePath = mReferenceDir.getPath(); 308 if (path.startsWith(referencePath)) { 309 int length = referencePath.length(); 310 if (path.length() > length && path.charAt(length) == File.separatorChar) { 311 length++; 312 } 313 314 return path.substring(length); 315 } 316 317 return path; 318 } 319 320 /** 321 * Returns the relative path of a given file within the current project. 322 * 323 * @param file the file under this project to check 324 * @return the path relative to the project 325 */ 326 @NonNull getRelativePath(@onNull File file)327 public String getRelativePath(@NonNull File file) { 328 String path = file.getPath(); 329 String referencePath = mDir.getPath(); 330 if (path.startsWith(referencePath)) { 331 int length = referencePath.length(); 332 if (path.length() > length && path.charAt(length) == File.separatorChar) { 333 length++; 334 } 335 336 return path.substring(length); 337 } 338 339 return path; 340 } 341 342 /** 343 * Returns the project root directory 344 * 345 * @return the dir 346 */ 347 @NonNull getDir()348 public File getDir() { 349 return mDir; 350 } 351 352 /** 353 * Returns the original user supplied directory where the lint search 354 * started. For example, if you run lint against {@code /tmp/foo}, and it 355 * finds a project to lint in {@code /tmp/foo/dev/src/project1}, then the 356 * {@code dir} is {@code /tmp/foo/dev/src/project1} and the 357 * {@code referenceDir} is {@code /tmp/foo/}. 358 * 359 * @return the reference directory, never null 360 */ 361 @NonNull getReferenceDir()362 public File getReferenceDir() { 363 return mReferenceDir; 364 } 365 366 /** 367 * Gets the configuration associated with this project 368 * 369 * @return the configuration associated with this project 370 */ 371 @NonNull getConfiguration()372 public Configuration getConfiguration() { 373 if (mConfiguration == null) { 374 mConfiguration = mClient.getConfiguration(this); 375 } 376 return mConfiguration; 377 } 378 379 /** 380 * Returns the application package specified by the manifest 381 * 382 * @return the application package, or null if unknown 383 */ 384 @Nullable getPackage()385 public String getPackage() { 386 //assert !mLibrary; // Should call getPackage on the master project, not the library 387 // Assertion disabled because you might be running lint on a standalone library project. 388 389 return mPackage; 390 } 391 392 /** 393 * Returns the minimum API level requested by the manifest, or -1 if not 394 * specified 395 * 396 * @return the minimum API level or -1 if unknown 397 */ getMinSdk()398 public int getMinSdk() { 399 //assert !mLibrary; // Should call getMinSdk on the master project, not the library 400 // Assertion disabled because you might be running lint on a standalone library project. 401 402 return mMinSdk; 403 } 404 405 /** 406 * Returns the target API level specified by the manifest, or -1 if not 407 * specified 408 * 409 * @return the target API level or -1 if unknown 410 */ getTargetSdk()411 public int getTargetSdk() { 412 //assert !mLibrary; // Should call getTargetSdk on the master project, not the library 413 // Assertion disabled because you might be running lint on a standalone library project. 414 415 return mTargetSdk; 416 } 417 418 /** 419 * Initialized the manifest state from the given manifest model 420 * 421 * @param document the DOM document for the manifest XML document 422 */ readManifest(@onNull Document document)423 public void readManifest(@NonNull Document document) { 424 Element root = document.getDocumentElement(); 425 if (root == null) { 426 return; 427 } 428 429 mPackage = root.getAttribute(ATTR_PACKAGE); 430 431 // Initialize minSdk and targetSdk 432 NodeList usesSdks = root.getElementsByTagName(TAG_USES_SDK); 433 if (usesSdks.getLength() > 0) { 434 Element element = (Element) usesSdks.item(0); 435 436 String minSdk = null; 437 if (element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { 438 minSdk = element.getAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); 439 } 440 if (minSdk != null) { 441 try { 442 mMinSdk = Integer.valueOf(minSdk); 443 } catch (NumberFormatException e) { 444 mMinSdk = 1; 445 } 446 } 447 448 String targetSdk = null; 449 if (element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { 450 targetSdk = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); 451 } else if (minSdk != null) { 452 targetSdk = minSdk; 453 } 454 if (targetSdk != null) { 455 try { 456 mTargetSdk = Integer.valueOf(targetSdk); 457 } catch (NumberFormatException e) { 458 // TODO: Handle codenames? 459 mTargetSdk = -1; 460 } 461 } 462 } else if (isAospBuildEnvironment()) { 463 extractAospMinSdkVersion(); 464 } 465 } 466 467 /** 468 * Returns true if this project is an Android library project 469 * 470 * @return true if this project is an Android library project 471 */ isLibrary()472 public boolean isLibrary() { 473 return mLibrary; 474 } 475 476 /** 477 * Returns the list of library projects referenced by this project 478 * 479 * @return the list of library projects referenced by this project, never 480 * null 481 */ 482 @NonNull getDirectLibraries()483 public List<Project> getDirectLibraries() { 484 return mDirectLibraries; 485 } 486 487 /** 488 * Returns the transitive closure of the library projects for this project 489 * 490 * @return the transitive closure of the library projects for this project 491 */ 492 @NonNull getAllLibraries()493 public List<Project> getAllLibraries() { 494 if (mAllLibraries == null) { 495 if (mDirectLibraries.size() == 0) { 496 return mDirectLibraries; 497 } 498 499 List<Project> all = new ArrayList<Project>(); 500 addLibraryProjects(all); 501 mAllLibraries = all; 502 } 503 504 return mAllLibraries; 505 } 506 507 /** 508 * Adds this project's library project and their library projects 509 * recursively into the given collection of projects 510 * 511 * @param collection the collection to add the projects into 512 */ addLibraryProjects(@onNull Collection<Project> collection)513 private void addLibraryProjects(@NonNull Collection<Project> collection) { 514 for (Project library : mDirectLibraries) { 515 collection.add(library); 516 // Recurse 517 library.addLibraryProjects(collection); 518 } 519 } 520 521 /** 522 * Gets the SDK info for the current project. 523 * 524 * @return the SDK info for the current project, never null 525 */ 526 @NonNull getSdkInfo()527 public SdkInfo getSdkInfo() { 528 if (mSdkInfo == null) { 529 mSdkInfo = mClient.getSdkInfo(this); 530 } 531 532 return mSdkInfo; 533 } 534 535 /** 536 * Gets the path to the manifest file in this project, if it exists 537 * 538 * @return the path to the manifest file, or null if it does not exist 539 */ getManifestFile()540 public File getManifestFile() { 541 File manifestFile = new File(mDir, ANDROID_MANIFEST_XML); 542 if (manifestFile.exists()) { 543 return manifestFile; 544 } 545 546 return null; 547 } 548 549 /** 550 * Returns the proguard path configured for this project, or null if ProGuard is 551 * not configured. 552 * 553 * @return the proguard path, or null 554 */ 555 @Nullable getProguardPath()556 public String getProguardPath() { 557 return mProguardPath; 558 } 559 560 /** 561 * Returns the name of the project 562 * 563 * @return the name of the project, never null 564 */ 565 @NonNull getName()566 public String getName() { 567 if (mName == null) { 568 // TODO: Consider reading the name from .project (if it's an Eclipse project) 569 mName = mDir.getName(); 570 } 571 572 return mName; 573 } 574 575 // --------------------------------------------------------------------------- 576 // Support for running lint on the AOSP source tree itself 577 578 private static Boolean sAospBuild; 579 580 /** Is lint running in an AOSP build environment */ isAospBuildEnvironment()581 private static boolean isAospBuildEnvironment() { 582 if (sAospBuild == null) { 583 sAospBuild = getAospTop() != null; 584 } 585 586 return sAospBuild.booleanValue(); 587 } 588 589 /** Get the root AOSP dir, if any */ getAospTop()590 private static String getAospTop() { 591 return System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ 592 } 593 594 /** Get the host out directory in AOSP, if any */ getAospHostOut()595 private static String getAospHostOut() { 596 return System.getenv("ANDROID_HOST_OUT"); //$NON-NLS-1$ 597 } 598 599 /** Get the product out directory in AOSP, if any */ getAospProductOut()600 private static String getAospProductOut() { 601 return System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ 602 } 603 getAospJavaSourcePath()604 private List<File> getAospJavaSourcePath() { 605 List<File> sources = new ArrayList<File>(2); 606 // Normal sources 607 File src = new File(mDir, "src"); //$NON-NLS-1$ 608 if (src.exists()) { 609 sources.add(src); 610 } 611 612 // Generates sources 613 for (File dir : getIntermediateDirs()) { 614 File classes = new File(dir, "src"); //$NON-NLS-1$ 615 if (classes.exists()) { 616 sources.add(classes); 617 } 618 } 619 620 if (sources.size() == 0) { 621 mClient.log(null, 622 "Warning: Could not find sources or generated sources for project %1$s", 623 getName()); 624 } 625 626 return sources; 627 } 628 getAospJavaClassPath()629 private List<File> getAospJavaClassPath() { 630 List<File> classDirs = new ArrayList<File>(1); 631 632 for (File dir : getIntermediateDirs()) { 633 File classes = new File(dir, "classes"); //$NON-NLS-1$ 634 if (classes.exists()) { 635 classDirs.add(classes); 636 } else { 637 classes = new File(dir, "classes.jar"); //$NON-NLS-1$ 638 if (classes.exists()) { 639 classDirs.add(classes); 640 } 641 } 642 } 643 644 if (classDirs.size() == 0) { 645 mClient.log(null, 646 "No bytecode found: Has the project been built? (%1$s)", getName()); 647 } 648 649 return classDirs; 650 } 651 652 /** Find the _intermediates directories for a given module name */ getIntermediateDirs()653 private List<File> getIntermediateDirs() { 654 // See build/core/definitions.mk and in particular the "intermediates-dir-for" definition 655 List<File> intermediates = new ArrayList<File>(); 656 657 // TODO: Look up the module name, e.g. LOCAL_MODULE. However, 658 // some Android.mk files do some complicated things with it - and most 659 // projects use the same module name as the directory name. 660 String moduleName = mDir.getName(); 661 662 String top = getAospTop(); 663 final String[] outFolders = new String[] { 664 top + "/out/host/common/obj", //$NON-NLS-1$ 665 top + "/out/target/common/obj", //$NON-NLS-1$ 666 getAospHostOut() + "/obj", //$NON-NLS-1$ 667 getAospProductOut() + "/obj" //$NON-NLS-1$ 668 }; 669 final String[] moduleClasses = new String[] { 670 "APPS", //$NON-NLS-1$ 671 "JAVA_LIBRARIES", //$NON-NLS-1$ 672 }; 673 674 for (String out : outFolders) { 675 assert new File(out.replace('/', File.separatorChar)).exists() : out; 676 for (String moduleClass : moduleClasses) { 677 String path = out + '/' + moduleClass + '/' + moduleName 678 + "_intermediates"; //$NON-NLS-1$ 679 File file = new File(path.replace('/', File.separatorChar)); 680 if (file.exists()) { 681 intermediates.add(file); 682 } 683 } 684 } 685 686 return intermediates; 687 } 688 extractAospMinSdkVersion()689 private void extractAospMinSdkVersion() { 690 // Is the SDK level specified by a Makefile? 691 boolean found = false; 692 File makefile = new File(mDir, "Android.mk"); //$NON-NLS-1$ 693 if (makefile.exists()) { 694 try { 695 List<String> lines = Files.readLines(makefile, Charsets.UTF_8); 696 Pattern p = Pattern.compile("LOCAL_SDK_VERSION\\s*:=\\s*(.*)"); //$NON-NLS-1$ 697 for (String line : lines) { 698 line = line.trim(); 699 Matcher matcher = p.matcher(line); 700 if (matcher.matches()) { 701 found = true; 702 String version = matcher.group(1); 703 if (version.equals("current")) { //$NON-NLS-1$ 704 mMinSdk = findCurrentAospVersion(); 705 } else { 706 try { 707 mMinSdk = Integer.valueOf(version); 708 } catch (NumberFormatException e) { 709 // Codename - just use current 710 mMinSdk = findCurrentAospVersion(); 711 } 712 } 713 break; 714 } 715 } 716 } catch (IOException ioe) { 717 mClient.log(ioe, null); 718 } 719 } 720 721 if (!found) { 722 mMinSdk = findCurrentAospVersion(); 723 } 724 } 725 726 /** Cache for {@link #findCurrentAospVersion()} */ 727 private static int sCurrentVersion; 728 729 /** In an AOSP build environment, identify the currently built image version, if available */ findCurrentAospVersion()730 private int findCurrentAospVersion() { 731 if (sCurrentVersion < 1) { 732 File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$ 733 .replace('/', File.separatorChar)); 734 File[] apiFiles = apiDir.listFiles(); 735 int max = 1; 736 for (File apiFile : apiFiles) { 737 String name = apiFile.getName(); 738 int index = name.indexOf('.'); 739 if (index > 0) { 740 String base = name.substring(0, index); 741 if (Character.isDigit(base.charAt(0))) { 742 try { 743 int version = Integer.parseInt(base); 744 if (version > max) { 745 max = version; 746 } 747 } catch (NumberFormatException nufe) { 748 // pass 749 } 750 } 751 } 752 } 753 sCurrentVersion = max; 754 } 755 756 return sCurrentVersion; 757 } 758 } 759