All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.detector.api.Project Maven / Gradle / Ivy

There is a newer version: 25.3.0
Show newest version
/*
 * 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.ANDROID_LIBRARY;
import static com.android.SdkConstants.ANDROID_LIBRARY_REFERENCE_FORMAT;
import static com.android.SdkConstants.ANDROID_MANIFEST_XML;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION;
import static com.android.SdkConstants.ATTR_PACKAGE;
import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION;
import static com.android.SdkConstants.PROGUARD_CONFIG;
import static com.android.SdkConstants.PROJECT_PROPERTIES;
import static com.android.SdkConstants.RES_FOLDER;
import static com.android.SdkConstants.TAG_USES_SDK;
import static com.android.SdkConstants.VALUE_TRUE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.sdk.SdkVersionInfo;
import com.android.tools.lint.client.api.CircularDependencyException;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.io.Closeables;
import com.google.common.io.Files;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A project contains information about an Android project being scanned for
 * Lint errors.
 * 

* 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. */ @Beta public class Project { private final LintClient mClient; private final File mDir; private final File mReferenceDir; private Configuration mConfiguration; private String mPackage; private int mMinSdk = 1; private int mTargetSdk = -1; private int mBuildSdk = -1; private boolean mLibrary; private String mName; private String mProguardPath; private boolean mMergeManifests; /** The SDK info, if any */ private SdkInfo mSdkInfo; /** * If non null, specifies a non-empty list of specific files under this * project which should be checked. */ private List mFiles; private List mJavaSourceFolders; private List mJavaClassFolders; private List mJavaLibraries; private List mDirectLibraries; private List mAllLibraries; private boolean mReportIssues = true; private Boolean mGradleProject; /** * Creates a new {@link Project} for the given directory. * * @param client the tool running the lint check * @param dir the root directory of the project * @param referenceDir See {@link #getReferenceDir()}. * @return a new {@link Project} */ @NonNull public static Project create( @NonNull LintClient client, @NonNull File dir, @NonNull File referenceDir) { return new Project(client, dir, referenceDir); } /** * Returns true if this project is a Gradle-based Android project * * @return true if this is a Gradle-based project */ public boolean isGradleProject() { if (mGradleProject == null) { mGradleProject = mClient.isGradleProject(this); } return mGradleProject; } /** Creates a new Project. Use one of the factory methods to create. */ private Project( @NonNull LintClient client, @NonNull File dir, @NonNull File referenceDir) { mClient = client; mDir = dir; mReferenceDir = referenceDir; try { // Read properties file and initialize library state Properties properties = new Properties(); File propFile = new File(dir, PROJECT_PROPERTIES); if (propFile.exists()) { @SuppressWarnings("resource") // Eclipse doesn't know about Closeables.closeQuietly BufferedInputStream is = new BufferedInputStream(new FileInputStream(propFile)); try { properties.load(is); String value = properties.getProperty(ANDROID_LIBRARY); mLibrary = VALUE_TRUE.equals(value); mProguardPath = properties.getProperty(PROGUARD_CONFIG); mMergeManifests = VALUE_TRUE.equals(properties.getProperty( "manifestmerger.enabled")); //$NON-NLS-1$ String target = properties.getProperty("target"); //$NON-NLS-1$ if (target != null) { int index = target.lastIndexOf('-'); if (index == -1) { index = target.lastIndexOf(':'); } if (index != -1) { String versionString = target.substring(index + 1); try { mBuildSdk = Integer.parseInt(versionString); } catch (NumberFormatException nufe) { mClient.log(Severity.WARNING, null, "Unexpected build target format: %1$s", target); } } } for (int i = 1; i < 1000; i++) { String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i); String library = properties.getProperty(key); if (library == null || library.isEmpty()) { // No holes in the numbering sequence is allowed break; } File libraryDir = new File(dir, library).getCanonicalFile(); if (mDirectLibraries == null) { mDirectLibraries = new ArrayList(); } // Adjust the reference dir to be a proper prefix path of the // library dir File libraryReferenceDir = referenceDir; if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { // Symlinks etc might have been resolved, so do those to // the reference dir as well libraryReferenceDir = libraryReferenceDir.getCanonicalFile(); if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { File file = libraryReferenceDir; while (file != null && !file.getPath().isEmpty()) { if (libraryDir.getPath().startsWith(file.getPath())) { libraryReferenceDir = file; break; } file = file.getParentFile(); } } } try { Project libraryPrj = client.getProject(libraryDir, libraryReferenceDir); mDirectLibraries.add(libraryPrj); // By default, we don't report issues in inferred library projects. // The driver will set report = true for those library explicitly // requested. libraryPrj.setReportIssues(false); } catch (CircularDependencyException e) { e.setProject(this); e.setLocation(Location.create(propFile)); throw e; } } } finally { Closeables.closeQuietly(is); } } } catch (IOException ioe) { client.log(ioe, "Initializing project state"); } if (mDirectLibraries != null) { mDirectLibraries = Collections.unmodifiableList(mDirectLibraries); } else { mDirectLibraries = Collections.emptyList(); } } @Override public String toString() { return "Project [dir=" + mDir + ']'; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mDir == null) ? 0 : mDir.hashCode()); return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Project other = (Project) obj; if (mDir == null) { if (other.mDir != null) return false; } else if (!mDir.equals(other.mDir)) return false; return true; } /** * Adds the given file to the list of files which should be checked in this * project. If no files are added, the whole project will be checked. * * @param file the file to be checked */ public void addFile(@NonNull File file) { if (mFiles == null) { mFiles = new ArrayList(); } mFiles.add(file); } /** * The list of files to be checked in this project. If null, the whole * project should be checked. * * @return the subset of files to be checked, or null for the whole project */ @Nullable public List getSubset() { return mFiles; } /** * Returns the list of source folders for Java source files * * @return a list of source folders to search for .java files */ @NonNull public List getJavaSourceFolders() { if (mJavaSourceFolders == null) { if (isAospFrameworksProject(mDir)) { return Collections.singletonList(new File(mDir, "java")); //$NON-NLS-1$ } if (isAospBuildEnvironment()) { String top = getAospTop(); if (mDir.getAbsolutePath().startsWith(top)) { mJavaSourceFolders = getAospJavaSourcePath(); return mJavaSourceFolders; } } mJavaSourceFolders = mClient.getJavaSourceFolders(this); } return mJavaSourceFolders; } /** * Returns the list of output folders for class files * @return a list of output folders to search for .class files */ @NonNull public List getJavaClassFolders() { if (mJavaClassFolders == null) { if (isAospFrameworksProject(mDir)) { File top = mDir.getParentFile().getParentFile().getParentFile(); if (top != null) { File out = new File(top, "out"); if (out.exists()) { String relative = "target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar"; File jar = new File(out, relative.replace('/', File.separatorChar)); if (jar.exists()) { mJavaClassFolders = Collections.singletonList(jar); return mJavaClassFolders; } } } } if (isAospBuildEnvironment()) { String top = getAospTop(); if (mDir.getAbsolutePath().startsWith(top)) { mJavaClassFolders = getAospJavaClassPath(); return mJavaClassFolders; } } mJavaClassFolders = mClient.getJavaClassFolders(this); } return mJavaClassFolders; } /** * Returns the list of Java libraries (typically .jar files) that this * project depends on. Note that this refers to jar libraries, not Android * library projects which are processed in a separate pass with their own * source and class folders. * * @return a list of .jar files (or class folders) that this project depends * on. */ @NonNull public List getJavaLibraries() { if (mJavaLibraries == null) { // AOSP builds already merge libraries and class folders into // the single classes.jar file, so these have already been processed // in getJavaClassFolders. mJavaLibraries = mClient.getJavaLibraries(this); } return mJavaLibraries; } /** * Returns the resource folder. * * @return a file pointing to the resource folder, or null if the project * does not contain any resources */ @NonNull public List getResourceFolders() { List folders = mClient.getResourceFolders(this); if (folders.size() == 1 && isAospFrameworksProject(mDir)) { // No manifest file for this project: just init the manifest values here mMinSdk = mTargetSdk = SdkVersionInfo.HIGHEST_KNOWN_API; File folder = new File(folders.get(0), RES_FOLDER); if (!folder.exists()) { folders = Collections.emptyList(); } } return folders; } /** * Returns the relative path of a given file relative to the user specified * directory (which is often the project directory but sometimes a higher up * directory when a directory tree is being scanned * * @param file the file under this project to check * @return the path relative to the reference directory (often the project directory) */ @NonNull public String getDisplayPath(@NonNull File file) { String path = file.getPath(); String referencePath = mReferenceDir.getPath(); if (path.startsWith(referencePath)) { int length = referencePath.length(); if (path.length() > length && path.charAt(length) == File.separatorChar) { length++; } return path.substring(length); } return path; } /** * Returns the relative path of a given file within the current project. * * @param file the file under this project to check * @return the path relative to the project */ @NonNull public String getRelativePath(@NonNull File file) { String path = file.getPath(); String referencePath = mDir.getPath(); if (path.startsWith(referencePath)) { int length = referencePath.length(); if (path.length() > length && path.charAt(length) == File.separatorChar) { length++; } return path.substring(length); } return path; } /** * Returns the project root directory * * @return the dir */ @NonNull public File getDir() { return mDir; } /** * Returns the original user supplied directory where the lint search * started. For example, if you run lint against {@code /tmp/foo}, and it * finds a project to lint in {@code /tmp/foo/dev/src/project1}, then the * {@code dir} is {@code /tmp/foo/dev/src/project1} and the * {@code referenceDir} is {@code /tmp/foo/}. * * @return the reference directory, never null */ @NonNull public File getReferenceDir() { return mReferenceDir; } /** * Gets the configuration associated with this project * * @return the configuration associated with this project */ @NonNull public Configuration getConfiguration() { if (mConfiguration == null) { mConfiguration = mClient.getConfiguration(this); } return mConfiguration; } /** * Returns the application package specified by the manifest * * @return the application package, or null if unknown */ @Nullable public String getPackage() { //assert !mLibrary; // Should call getPackage on the master project, not the library // Assertion disabled because you might be running lint on a standalone library project. return mPackage; } /** * Returns the minimum API level requested by the manifest, or -1 if not * specified * * @return the minimum API level or -1 if unknown */ public int getMinSdk() { //assert !mLibrary; // Should call getMinSdk on the master project, not the library // Assertion disabled because you might be running lint on a standalone library project. return mMinSdk; } /** * Returns the target API level specified by the manifest, or -1 if not * specified * * @return the target API level or -1 if unknown */ public int getTargetSdk() { //assert !mLibrary; // Should call getTargetSdk on the master project, not the library // Assertion disabled because you might be running lint on a standalone library project. return mTargetSdk; } /** * Returns the target API used to build the project, or -1 if not known * * @return the build target API or -1 if unknown */ public int getBuildSdk() { return mBuildSdk; } /** * Initialized the manifest state from the given manifest model * * @param document the DOM document for the manifest XML document */ public void readManifest(@NonNull Document document) { Element root = document.getDocumentElement(); if (root == null) { return; } mPackage = root.getAttribute(ATTR_PACKAGE); // Initialize minSdk and targetSdk NodeList usesSdks = root.getElementsByTagName(TAG_USES_SDK); if (usesSdks.getLength() > 0) { Element element = (Element) usesSdks.item(0); String minSdk = null; if (element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { minSdk = element.getAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); } if (minSdk != null) { try { mMinSdk = Integer.valueOf(minSdk); } catch (NumberFormatException e) { mMinSdk = 1; } } String targetSdk = null; if (element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { targetSdk = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); } else if (minSdk != null) { targetSdk = minSdk; } if (targetSdk != null) { try { mTargetSdk = Integer.valueOf(targetSdk); } catch (NumberFormatException e) { // TODO: Handle codenames? mTargetSdk = -1; } } } else if (isAospBuildEnvironment()) { extractAospMinSdkVersion(); } } /** * Returns true if this project is an Android library project * * @return true if this project is an Android library project */ public boolean isLibrary() { return mLibrary; } /** * Returns the list of library projects referenced by this project * * @return the list of library projects referenced by this project, never * null */ @NonNull public List getDirectLibraries() { return mDirectLibraries; } /** * Returns the transitive closure of the library projects for this project * * @return the transitive closure of the library projects for this project */ @NonNull public List getAllLibraries() { if (mAllLibraries == null) { if (mDirectLibraries.isEmpty()) { return mDirectLibraries; } List all = new ArrayList(); addLibraryProjects(all); mAllLibraries = all; } return mAllLibraries; } /** * Adds this project's library project and their library projects * recursively into the given collection of projects * * @param collection the collection to add the projects into */ private void addLibraryProjects(@NonNull Collection collection) { for (Project library : mDirectLibraries) { collection.add(library); // Recurse library.addLibraryProjects(collection); } } /** * Gets the SDK info for the current project. * * @return the SDK info for the current project, never null */ @NonNull public SdkInfo getSdkInfo() { if (mSdkInfo == null) { mSdkInfo = mClient.getSdkInfo(this); } return mSdkInfo; } /** * Gets the path to the manifest file in this project, if it exists * * @return the path to the manifest file, or null if it does not exist */ @Nullable public File getManifestFile() { File manifestFile = new File(mDir, ANDROID_MANIFEST_XML); if (manifestFile.exists()) { return manifestFile; } return null; } /** * Returns the proguard path configured for this project, or null if ProGuard is * not configured. * * @return the proguard path, or null */ @Nullable public String getProguardPath() { return mProguardPath; } /** * Returns the name of the project * * @return the name of the project, never null */ @NonNull public String getName() { if (mName == null) { mName = mClient.getProjectName(this); } return mName; } /** * Sets the name of the project * * @param name the name of the project, never null */ public void setName(@NonNull String name) { assert !name.isEmpty(); mName = name; } /** * Sets whether lint should report issues in this project. See * {@link #getReportIssues()} for a full description of what that means. * * @param reportIssues whether lint should report issues in this project */ public void setReportIssues(boolean reportIssues) { mReportIssues = reportIssues; } /** * Returns whether lint should report issues in this project. *

* If a user specifies a project and its library projects for analysis, then * those library projects are all "included", and all errors found in all * the projects are reported. But if the user is only running lint on the * main project, we shouldn't report errors in any of the library projects. * We still need to consider them for certain types of checks, such * as determining whether resources found in the main project are unused, so * the detectors must still get a chance to look at these projects. The * {@code #getReportIssues()} attribute is used for this purpose. * * @return whether lint should report issues in this project */ public boolean getReportIssues() { return mReportIssues; } /** * Sets whether manifest merging is in effect. * * @param merging whether manifest merging is in effect */ public void setMergingManifests(boolean merging) { mMergeManifests = merging; } /** * Returns whether manifest merging is in effect * * @return true if manifests in library projects should be merged into main projects */ public boolean isMergingManifests() { return mMergeManifests; } // --------------------------------------------------------------------------- // Support for running lint on the AOSP source tree itself private static Boolean sAospBuild; /** Is lint running in an AOSP build environment */ private static boolean isAospBuildEnvironment() { if (sAospBuild == null) { sAospBuild = getAospTop() != null; } return sAospBuild.booleanValue(); } /** * Is this the frameworks AOSP project? Needs some hardcoded support since * it doesn't have a manifest file, etc. * * @param dir the project directory to check * @return true if this looks like the frameworks/base/core project */ static boolean isAospFrameworksProject(@NonNull File dir) { if (!dir.getPath().endsWith("core")) { //$NON-NLS-1$ return false; } File parent = dir.getParentFile(); if (parent == null || !parent.getName().equals("base")) { //$NON-NLS-1$ return false; } parent = parent.getParentFile(); if (parent == null || !parent.getName().equals("frameworks")) { //$NON-NLS-1$ return false; } return true; } /** Get the root AOSP dir, if any */ private static String getAospTop() { return System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ } /** Get the host out directory in AOSP, if any */ private static String getAospHostOut() { return System.getenv("ANDROID_HOST_OUT"); //$NON-NLS-1$ } /** Get the product out directory in AOSP, if any */ private static String getAospProductOut() { return System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ } private List getAospJavaSourcePath() { List sources = new ArrayList(2); // Normal sources File src = new File(mDir, "src"); //$NON-NLS-1$ if (src.exists()) { sources.add(src); } // Generates sources for (File dir : getIntermediateDirs()) { File classes = new File(dir, "src"); //$NON-NLS-1$ if (classes.exists()) { sources.add(classes); } } if (sources.isEmpty()) { mClient.log(null, "Warning: Could not find sources or generated sources for project %1$s", getName()); } return sources; } private List getAospJavaClassPath() { List classDirs = new ArrayList(1); for (File dir : getIntermediateDirs()) { File classes = new File(dir, "classes"); //$NON-NLS-1$ if (classes.exists()) { classDirs.add(classes); } else { classes = new File(dir, "classes.jar"); //$NON-NLS-1$ if (classes.exists()) { classDirs.add(classes); } } } if (classDirs.isEmpty()) { mClient.log(null, "No bytecode found: Has the project been built? (%1$s)", getName()); } return classDirs; } /** Find the _intermediates directories for a given module name */ private List getIntermediateDirs() { // See build/core/definitions.mk and in particular the "intermediates-dir-for" definition List intermediates = new ArrayList(); // TODO: Look up the module name, e.g. LOCAL_MODULE. However, // some Android.mk files do some complicated things with it - and most // projects use the same module name as the directory name. String moduleName = mDir.getName(); String top = getAospTop(); final String[] outFolders = new String[] { top + "/out/host/common/obj", //$NON-NLS-1$ top + "/out/target/common/obj", //$NON-NLS-1$ getAospHostOut() + "/obj", //$NON-NLS-1$ getAospProductOut() + "/obj" //$NON-NLS-1$ }; final String[] moduleClasses = new String[] { "APPS", //$NON-NLS-1$ "JAVA_LIBRARIES", //$NON-NLS-1$ }; for (String out : outFolders) { assert new File(out.replace('/', File.separatorChar)).exists() : out; for (String moduleClass : moduleClasses) { String path = out + '/' + moduleClass + '/' + moduleName + "_intermediates"; //$NON-NLS-1$ File file = new File(path.replace('/', File.separatorChar)); if (file.exists()) { intermediates.add(file); } } } return intermediates; } private void extractAospMinSdkVersion() { // Is the SDK level specified by a Makefile? boolean found = false; File makefile = new File(mDir, "Android.mk"); //$NON-NLS-1$ if (makefile.exists()) { try { List lines = Files.readLines(makefile, Charsets.UTF_8); Pattern p = Pattern.compile("LOCAL_SDK_VERSION\\s*:=\\s*(.*)"); //$NON-NLS-1$ for (String line : lines) { line = line.trim(); Matcher matcher = p.matcher(line); if (matcher.matches()) { found = true; String version = matcher.group(1); if (version.equals("current")) { //$NON-NLS-1$ mMinSdk = findCurrentAospVersion(); } else { try { mMinSdk = Integer.valueOf(version); } catch (NumberFormatException e) { // Codename - just use current mMinSdk = findCurrentAospVersion(); } } break; } } } catch (IOException ioe) { mClient.log(ioe, null); } } if (!found) { mMinSdk = findCurrentAospVersion(); } } /** Cache for {@link #findCurrentAospVersion()} */ private static int sCurrentVersion; /** In an AOSP build environment, identify the currently built image version, if available */ private static int findCurrentAospVersion() { if (sCurrentVersion < 1) { File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$ .replace('/', File.separatorChar)); File[] apiFiles = apiDir.listFiles(); if (apiFiles == null) { sCurrentVersion = 1; return sCurrentVersion; } int max = 1; for (File apiFile : apiFiles) { String name = apiFile.getName(); int index = name.indexOf('.'); if (index > 0) { String base = name.substring(0, index); if (Character.isDigit(base.charAt(0))) { try { int version = Integer.parseInt(base); if (version > max) { max = version; } } catch (NumberFormatException nufe) { // pass } } } } sCurrentVersion = max; } return sCurrentVersion; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy