Java tutorial
/* * 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.client.api; import static com.android.SdkConstants.ANDROID_MANIFEST_XML; import static com.android.SdkConstants.ATTR_IGNORE; import static com.android.SdkConstants.CLASS_CONSTRUCTOR; import static com.android.SdkConstants.CONSTRUCTOR_NAME; import static com.android.SdkConstants.DOT_CLASS; import static com.android.SdkConstants.DOT_JAR; import static com.android.SdkConstants.DOT_JAVA; import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; import static com.android.SdkConstants.OLD_PROGUARD_FILE; import static com.android.SdkConstants.RES_FOLDER; import static com.android.SdkConstants.SUPPRESS_ALL; import static com.android.SdkConstants.SUPPRESS_LINT; import static com.android.SdkConstants.TOOLS_URI; import static com.android.tools.lint.detector.api.LintUtils.isAnonymousClass; import static org.objectweb.asm.Opcodes.ASM4; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; import com.android.tools.lint.client.api.LintListener.EventType; import com.android.tools.lint.detector.api.ClassContext; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.JavaContext; import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Project; import com.android.tools.lint.detector.api.ResourceXmlDetector; import com.android.tools.lint.detector.api.Scope; import com.android.tools.lint.detector.api.Severity; import com.android.tools.lint.detector.api.XmlContext; import com.google.common.annotations.Beta; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.w3c.dom.Attr; import org.w3c.dom.Element; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import lombok.ast.Annotation; import lombok.ast.AnnotationElement; import lombok.ast.AnnotationValue; import lombok.ast.ArrayInitializer; import lombok.ast.ClassDeclaration; import lombok.ast.ConstructorDeclaration; import lombok.ast.Expression; import lombok.ast.MethodDeclaration; import lombok.ast.Modifiers; import lombok.ast.Node; import lombok.ast.StrictListAccessor; import lombok.ast.StringLiteral; import lombok.ast.TypeReference; import lombok.ast.VariableDefinition; /** * Analyzes Android projects and 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 LintDriver { /** * Max number of passes to run through the lint runner if requested by * {@link #requestRepeat} */ private static final int MAX_PHASES = 3; private static final String SUPPRESS_LINT_VMSIG = '/' + SUPPRESS_LINT + ';'; private final LintClient mClient; private LintRequest mRequest; private final IssueRegistry mRegistry; private volatile boolean mCanceled; private EnumSet<Scope> mScope; private List<? extends Detector> mApplicableDetectors; private Map<Scope, List<Detector>> mScopeDetectors; private List<LintListener> mListeners; private int mPhase; private List<Detector> mRepeatingDetectors; private EnumSet<Scope> mRepeatScope; private Project[] mCurrentProjects; private Project mCurrentProject; private boolean mAbbreviating = true; private boolean mParserErrors; private Map<Object, Object> mProperties; /** * Creates a new {@link LintDriver} * * @param registry The registry containing issues to be checked * @param client the tool wrapping the analyzer, such as an IDE or a CLI */ public LintDriver(@NonNull IssueRegistry registry, @NonNull LintClient client) { mRegistry = registry; mClient = new LintClientWrapper(client); } /** Cancels the current lint run as soon as possible */ public void cancel() { mCanceled = true; } /** * Returns the scope for the lint job * * @return the scope, never null */ @NonNull public EnumSet<Scope> getScope() { return mScope; } /** * Returns the lint client requesting the lint check. This may not be the same * instance as the one passed in to this driver; lint uses a wrapper which performs * additional validation to ensure that for example badly behaved detectors which report * issues that have been disabled will get muted without the real lint client getting * notified. Thus, this {@link LintClient} is suitable for use by detectors to look * up a client to for example get location handles from, but tool handling code should * never try to cast this client back to their original lint client. For the original * lint client, use {@link LintRequest} instead. * * @return the client, never null */ @NonNull public LintClient getClient() { return mClient; } /** * Returns the current request, which points to the original files to be checked, * the original scope, the original {@link LintClient}, as well as the release mode. * * @return the request */ @NonNull public LintRequest getRequest() { return mRequest; } /** * Records a property for later retrieval by {@link #getProperty(Object)} * * @param key the key to associate the value with * @param value the value, or null to remove a previous binding */ public void putProperty(@NonNull Object key, @Nullable Object value) { if (mProperties == null) { mProperties = Maps.newHashMap(); } if (value == null) { mProperties.remove(key); } else { mProperties.put(key, value); } } /** * Returns the property previously stored with the given key, or null * * @param key the key * @return the value or null if not found */ @Nullable public Object getProperty(@NonNull Object key) { if (mProperties != null) { return mProperties.get(key); } return null; } /** * Returns the current phase number. The first pass is numbered 1. Only one pass * will be performed, unless a {@link Detector} calls {@link #requestRepeat}. * * @return the current phase, usually 1 */ public int getPhase() { return mPhase; } /** * Returns the current {@link IssueRegistry}. * * @return the current {@link IssueRegistry} */ @NonNull public IssueRegistry getRegistry() { return mRegistry; } /** * Returns the project containing a given file, or null if not found. This searches * only among the currently checked project and its library projects, not among all * possible projects being scanned sequentially. * * @param file the file to be checked * @return the corresponding project, or null if not found */ @Nullable public Project findProjectFor(@NonNull File file) { if (mCurrentProjects != null) { if (mCurrentProjects.length == 1) { return mCurrentProjects[0]; } String path = file.getPath(); for (Project project : mCurrentProjects) { if (path.startsWith(project.getDir().getPath())) { return project; } } } return null; } /** * Sets whether lint should abbreviate output when appropriate. * * @param abbreviating true to abbreviate output, false to include everything */ public void setAbbreviating(boolean abbreviating) { mAbbreviating = abbreviating; } /** * Returns whether lint should abbreviate output when appropriate. * * @return true if lint should abbreviate output, false when including everything */ public boolean isAbbreviating() { return mAbbreviating; } /** * Returns whether lint has encountered any files with fatal parser errors * (e.g. broken source code, or even broken parsers) * <p> * This is useful for checks that need to make sure they've seen all data in * order to be conclusive (such as an unused resource check). * * @return true if any files were not properly processed because they * contained parser errors */ public boolean hasParserErrors() { return mParserErrors; } /** * Sets whether lint has encountered files with fatal parser errors. * * @see #hasParserErrors() * @param hasErrors whether parser errors have been encountered */ public void setHasParserErrors(boolean hasErrors) { mParserErrors = hasErrors; } /** * Returns the projects being analyzed * * @return the projects being analyzed */ @NonNull public List<Project> getProjects() { if (mCurrentProjects != null) { return Arrays.asList(mCurrentProjects); } return Collections.emptyList(); } /** * Analyze the given file (which can point to an Android project). Issues found * are reported to the associated {@link LintClient}. * * @param files the files and directories to be analyzed * @param scope the scope of the analysis; detectors with a wider scope will * not be run. If null, the scope will be inferred from the files. * @deprecated use {@link #analyze(LintRequest) instead} */ @Deprecated public void analyze(@NonNull List<File> files, @Nullable EnumSet<Scope> scope) { analyze(new LintRequest(mClient, files).setScope(scope)); } /** * Analyze the given files (which can point to Android projects or directories * containing Android projects). Issues found are reported to the associated * {@link LintClient}. * <p> * Note that the {@link LintDriver} is not multi thread safe or re-entrant; * if you want to run potentially overlapping lint jobs, create a separate driver * for each job. * * @param request the files and directories to be analyzed */ public void analyze(@NonNull LintRequest request) { try { mRequest = request; analyze(); } finally { mRequest = null; } } /** Runs the driver to analyze the requested files */ private void analyze() { mCanceled = false; mScope = mRequest.getScope(); Collection<Project> projects; try { projects = computeProjects(mRequest.getFiles()); } catch (CircularDependencyException e) { mCurrentProject = e.getProject(); if (mCurrentProject != null) { File file = e.getLocation().getFile(); Context context = new Context(this, mCurrentProject, null, file); context.report(IssueRegistry.LINT_ERROR, e.getLocation(), e.getMessage(), null); mCurrentProject = null; } return; } if (projects.isEmpty()) { mClient.log(null, "No projects found for %1$s", mRequest.getFiles().toString()); return; } if (mCanceled) { return; } if (mScope == null) { // Infer the scope mScope = EnumSet.noneOf(Scope.class); for (Project project : projects) { List<File> subset = project.getSubset(); if (subset != null) { for (File file : subset) { String name = file.getName(); if (name.equals(ANDROID_MANIFEST_XML)) { mScope.add(Scope.MANIFEST); } else if (name.endsWith(DOT_XML)) { mScope.add(Scope.RESOURCE_FILE); } else if (name.equals(RES_FOLDER) || file.getParent().equals(RES_FOLDER)) { mScope.add(Scope.ALL_RESOURCE_FILES); mScope.add(Scope.RESOURCE_FILE); } else if (name.endsWith(DOT_JAVA)) { mScope.add(Scope.JAVA_FILE); } else if (name.endsWith(DOT_CLASS)) { mScope.add(Scope.CLASS_FILE); } else if (name.equals(OLD_PROGUARD_FILE) || name.equals(FN_PROJECT_PROGUARD_FILE)) { mScope.add(Scope.PROGUARD_FILE); } } } else { // Specified a full project: just use the full project scope mScope = Scope.ALL; break; } } } fireEvent(EventType.STARTING, null); for (Project project : projects) { mPhase = 1; // The set of available detectors varies between projects computeDetectors(project); if (mApplicableDetectors.isEmpty()) { // No detectors enabled in this project: skip it continue; } checkProject(project); if (mCanceled) { break; } runExtraPhases(project); } fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null); } private void runExtraPhases(Project project) { // Did any detectors request another phase? if (mRepeatingDetectors != null) { // Yes. Iterate up to MAX_PHASES times. // During the extra phases, we might be narrowing the scope, and setting it in the // scope field such that detectors asking about the available scope will get the // correct result. However, we need to restore it to the original scope when this // is done in case there are other projects that will be checked after this, since // the repeated phases is done *per project*, not after all projects have been // processed. EnumSet<Scope> oldScope = mScope; do { mPhase++; fireEvent(EventType.NEW_PHASE, new Context(this, project, null, project.getDir())); // Narrow the scope down to the set of scopes requested by // the rules. if (mRepeatScope == null) { mRepeatScope = Scope.ALL; } mScope = Scope.intersect(mScope, mRepeatScope); if (mScope.isEmpty()) { break; } // Compute the detectors to use for this pass. // Unlike the normal computeDetectors(project) call, // this is going to use the existing instances, and include // those that apply for the configuration. computeRepeatingDetectors(mRepeatingDetectors, project); if (mApplicableDetectors.isEmpty()) { // No detectors enabled in this project: skip it continue; } checkProject(project); if (mCanceled) { break; } } while (mPhase < MAX_PHASES && mRepeatingDetectors != null); mScope = oldScope; } } private void computeRepeatingDetectors(List<Detector> detectors, Project project) { // Ensure that the current visitor is recomputed mCurrentFolderType = null; mCurrentVisitor = null; // Create map from detector class to issue such that we can // compute applicable issues for each detector in the list of detectors // to be repeated List<Issue> issues = mRegistry.getIssues(); Multimap<Class<? extends Detector>, Issue> issueMap = ArrayListMultimap.create(issues.size(), 3); for (Issue issue : issues) { issueMap.put(issue.getImplementation().getDetectorClass(), issue); } Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope = new HashMap<Class<? extends Detector>, EnumSet<Scope>>(); Map<Scope, List<Detector>> scopeToDetectors = new EnumMap<Scope, List<Detector>>(Scope.class); List<Detector> detectorList = new ArrayList<Detector>(); // Compute the list of detectors (narrowed down from mRepeatingDetectors), // and simultaneously build up the detectorToScope map which tracks // the scopes each detector is affected by (this is used to populate // the mScopeDetectors map which is used during iteration). Configuration configuration = project.getConfiguration(); for (Detector detector : detectors) { Class<? extends Detector> detectorClass = detector.getClass(); Collection<Issue> detectorIssues = issueMap.get(detectorClass); if (detectorIssues != null) { boolean add = false; for (Issue issue : detectorIssues) { // The reason we have to check whether the detector is enabled // is that this is a per-project property, so when running lint in multiple // projects, a detector enabled only in a different project could have // requested another phase, and we end up in this project checking whether // the detector is enabled here. if (!configuration.isEnabled(issue)) { continue; } add = true; // Include detector if any of its issues are enabled EnumSet<Scope> s = detectorToScope.get(detectorClass); EnumSet<Scope> issueScope = issue.getImplementation().getScope(); if (s == null) { detectorToScope.put(detectorClass, issueScope); } else if (!s.containsAll(issueScope)) { EnumSet<Scope> union = EnumSet.copyOf(s); union.addAll(issueScope); detectorToScope.put(detectorClass, union); } } if (add) { detectorList.add(detector); EnumSet<Scope> union = detectorToScope.get(detector.getClass()); for (Scope s : union) { List<Detector> list = scopeToDetectors.get(s); if (list == null) { list = new ArrayList<Detector>(); scopeToDetectors.put(s, list); } list.add(detector); } } } } mApplicableDetectors = detectorList; mScopeDetectors = scopeToDetectors; mRepeatingDetectors = null; mRepeatScope = null; validateScopeList(); } private void computeDetectors(@NonNull Project project) { // Ensure that the current visitor is recomputed mCurrentFolderType = null; mCurrentVisitor = null; Configuration configuration = project.getConfiguration(); mScopeDetectors = new EnumMap<Scope, List<Detector>>(Scope.class); mApplicableDetectors = mRegistry.createDetectors(mClient, configuration, mScope, mScopeDetectors); validateScopeList(); } /** Development diagnostics only, run with assertions on */ @SuppressWarnings("all") // Turn off warnings for the intentional assertion side effect below private void validateScopeList() { boolean assertionsEnabled = false; assert assertionsEnabled = true; // Intentional side-effect if (assertionsEnabled) { List<Detector> resourceFileDetectors = mScopeDetectors.get(Scope.RESOURCE_FILE); if (resourceFileDetectors != null) { for (Detector detector : resourceFileDetectors) { assert detector instanceof ResourceXmlDetector : detector; } } List<Detector> manifestDetectors = mScopeDetectors.get(Scope.MANIFEST); if (manifestDetectors != null) { for (Detector detector : manifestDetectors) { assert detector instanceof Detector.XmlScanner : detector; } } List<Detector> javaCodeDetectors = mScopeDetectors.get(Scope.ALL_JAVA_FILES); if (javaCodeDetectors != null) { for (Detector detector : javaCodeDetectors) { assert detector instanceof Detector.JavaScanner : detector; } } List<Detector> javaFileDetectors = mScopeDetectors.get(Scope.JAVA_FILE); if (javaFileDetectors != null) { for (Detector detector : javaFileDetectors) { assert detector instanceof Detector.JavaScanner : detector; } } List<Detector> classDetectors = mScopeDetectors.get(Scope.CLASS_FILE); if (classDetectors != null) { for (Detector detector : classDetectors) { assert detector instanceof Detector.ClassScanner : detector; } } List<Detector> classCodeDetectors = mScopeDetectors.get(Scope.ALL_CLASS_FILES); if (classCodeDetectors != null) { for (Detector detector : classCodeDetectors) { assert detector instanceof Detector.ClassScanner : detector; } } List<Detector> otherDetectors = mScopeDetectors.get(Scope.OTHER); if (otherDetectors != null) { for (Detector detector : otherDetectors) { assert detector instanceof Detector.OtherFileScanner : detector; } } } } private void registerProjectFile(@NonNull Map<File, Project> fileToProject, @NonNull File file, @NonNull File projectDir, @NonNull File rootDir) { fileToProject.put(file, mClient.getProject(projectDir, rootDir)); } private Collection<Project> computeProjects(@NonNull List<File> files) { // Compute list of projects Map<File, Project> fileToProject = new HashMap<File, Project>(); File sharedRoot = null; // Ensure that we have absolute paths such that if you lint // "foo bar" in "baz" we can show baz/ as the root if (files.size() > 1) { List<File> absolute = new ArrayList<File>(files.size()); for (File file : files) { absolute.add(file.getAbsoluteFile()); } files = absolute; sharedRoot = LintUtils.getCommonParent(files); if (sharedRoot != null && sharedRoot.getParentFile() == null) { // "/" ? sharedRoot = null; } } for (File file : files) { if (file.isDirectory()) { File rootDir = sharedRoot; if (rootDir == null) { rootDir = file; if (files.size() > 1) { rootDir = file.getParentFile(); if (rootDir == null) { rootDir = file; } } } // Figure out what to do with a directory. Note that the meaning of the // directory can be ambiguous: // If you pass a directory which is unknown, we don't know if we should // search upwards (in case you're pointing at a deep java package folder // within the project), or if you're pointing at some top level directory // containing lots of projects you want to scan. We attempt to do the // right thing, which is to see if you're pointing right at a project or // right within it (say at the src/ or res/) folder, and if not, you're // hopefully pointing at a project tree that you want to scan recursively. if (LintUtils.isProjectDir(file)) { registerProjectFile(fileToProject, file, file, rootDir); continue; } else { File parent = file.getParentFile(); if (parent != null) { if (LintUtils.isProjectDir(parent)) { registerProjectFile(fileToProject, file, parent, parent); continue; } else { parent = parent.getParentFile(); if (parent != null && LintUtils.isProjectDir(parent)) { registerProjectFile(fileToProject, file, parent, parent); continue; } } } // Search downwards for nested projects addProjects(file, fileToProject, rootDir); } } else { // Pointed at a file: Search upwards for the containing project File parent = file.getParentFile(); while (parent != null) { if (LintUtils.isProjectDir(parent)) { registerProjectFile(fileToProject, file, parent, parent); break; } parent = parent.getParentFile(); } } if (mCanceled) { return Collections.emptySet(); } } for (Map.Entry<File, Project> entry : fileToProject.entrySet()) { File file = entry.getKey(); Project project = entry.getValue(); if (!file.equals(project.getDir())) { if (file.isDirectory()) { try { File dir = file.getCanonicalFile(); if (dir.equals(project.getDir())) { continue; } } catch (IOException ioe) { // pass } } project.addFile(file); } } // Partition the projects up such that we only return projects that aren't // included by other projects (e.g. because they are library projects) Collection<Project> allProjects = fileToProject.values(); Set<Project> roots = new HashSet<Project>(allProjects); for (Project project : allProjects) { roots.removeAll(project.getAllLibraries()); } // Report issues for all projects that are explicitly referenced. We need to // do this here, since the project initialization will mark all library // projects as no-report projects by default. for (Project project : allProjects) { // Report issues for all projects explicitly listed or found via a directory // traversal -- including library projects. project.setReportIssues(true); } if (LintUtils.assertionsEnabled()) { // Make sure that all the project directories are unique. This ensures // that we didn't accidentally end up with different project instances // for a library project discovered as a directory as well as one // initialized from the library project dependency list IdentityHashMap<Project, Project> projects = new IdentityHashMap<Project, Project>(); for (Project project : roots) { projects.put(project, project); for (Project library : project.getAllLibraries()) { projects.put(library, library); } } Set<File> dirs = new HashSet<File>(); for (Project project : projects.keySet()) { assert !dirs.contains(project.getDir()); dirs.add(project.getDir()); } } return roots; } private void addProjects(@NonNull File dir, @NonNull Map<File, Project> fileToProject, @NonNull File rootDir) { if (mCanceled) { return; } if (LintUtils.isProjectDir(dir)) { registerProjectFile(fileToProject, dir, dir, rootDir); } else { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { addProjects(file, fileToProject, rootDir); } } } } } private void checkProject(@NonNull Project project) { File projectDir = project.getDir(); Context projectContext = new Context(this, project, null, projectDir); fireEvent(EventType.SCANNING_PROJECT, projectContext); List<Project> allLibraries = project.getAllLibraries(); Set<Project> allProjects = new HashSet<Project>(allLibraries.size() + 1); allProjects.add(project); allProjects.addAll(allLibraries); mCurrentProjects = allProjects.toArray(new Project[allProjects.size()]); mCurrentProject = project; for (Detector check : mApplicableDetectors) { check.beforeCheckProject(projectContext); if (mCanceled) { return; } } assert mCurrentProject == project; runFileDetectors(project, project); if (!Scope.checkSingleFile(mScope)) { List<Project> libraries = project.getDirectLibraries(); for (Project library : libraries) { Context libraryContext = new Context(this, library, project, projectDir); fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext); mCurrentProject = library; for (Detector check : mApplicableDetectors) { check.beforeCheckLibraryProject(libraryContext); if (mCanceled) { return; } } assert mCurrentProject == library; runFileDetectors(library, project); if (mCanceled) { return; } assert mCurrentProject == library; for (Detector check : mApplicableDetectors) { check.afterCheckLibraryProject(libraryContext); if (mCanceled) { return; } } } } mCurrentProject = project; for (Detector check : mApplicableDetectors) { check.afterCheckProject(projectContext); if (mCanceled) { return; } } if (mCanceled) { mClient.report(projectContext, // Must provide an issue since API guarantees that the issue parameter IssueRegistry.CANCELLED, Severity.INFORMATIONAL, null /*range*/, "Lint canceled by user", null); } mCurrentProjects = null; } private void runFileDetectors(@NonNull Project project, @Nullable Project main) { // Look up manifest information (but not for library projects) File manifestFile = project.getManifestFile(); if (manifestFile != null) { XmlContext context = new XmlContext(this, project, main, manifestFile, null); IDomParser parser = mClient.getDomParser(); if (parser != null) { context.document = parser.parseXml(context); if (context.document != null) { try { project.readManifest(context.document); if ((!project.isLibrary() || (main != null && main.isMergingManifests())) && mScope.contains(Scope.MANIFEST)) { List<Detector> detectors = mScopeDetectors.get(Scope.MANIFEST); if (detectors != null) { XmlVisitor v = new XmlVisitor(parser, detectors); fireEvent(EventType.SCANNING_FILE, context); v.visitFile(context, manifestFile); } } } finally { if (context.document != null) { // else: freed by XmlVisitor above parser.dispose(context, context.document); } } } } } // Process both Scope.RESOURCE_FILE and Scope.ALL_RESOURCE_FILES detectors together // in a single pass through the resource directories. if (mScope.contains(Scope.ALL_RESOURCE_FILES) || mScope.contains(Scope.RESOURCE_FILE)) { List<Detector> checks = union(mScopeDetectors.get(Scope.RESOURCE_FILE), mScopeDetectors.get(Scope.ALL_RESOURCE_FILES)); if (checks != null && !checks.isEmpty()) { List<ResourceXmlDetector> xmlDetectors = new ArrayList<ResourceXmlDetector>(checks.size()); for (Detector detector : checks) { if (detector instanceof ResourceXmlDetector) { xmlDetectors.add((ResourceXmlDetector) detector); } } if (!xmlDetectors.isEmpty()) { List<File> files = project.getSubset(); if (files != null) { checkIndividualResources(project, main, xmlDetectors, files); } else { List<File> resourceFolders = project.getResourceFolders(); if (!resourceFolders.isEmpty() && !xmlDetectors.isEmpty()) { for (File res : resourceFolders) { checkResFolder(project, main, res, xmlDetectors); } } } } } } if (mCanceled) { return; } if (mScope.contains(Scope.JAVA_FILE) || mScope.contains(Scope.ALL_JAVA_FILES)) { List<Detector> checks = union(mScopeDetectors.get(Scope.JAVA_FILE), mScopeDetectors.get(Scope.ALL_JAVA_FILES)); if (checks != null && !checks.isEmpty()) { List<File> files = project.getSubset(); if (files != null) { checkIndividualJavaFiles(project, main, checks, files); } else { List<File> sourceFolders = project.getJavaSourceFolders(); checkJava(project, main, sourceFolders, checks); } } } if (mCanceled) { return; } if (mScope.contains(Scope.CLASS_FILE) || mScope.contains(Scope.ALL_CLASS_FILES) || mScope.contains(Scope.JAVA_LIBRARIES)) { checkClasses(project, main); } if (mScope.contains(Scope.OTHER)) { List<Detector> checks = mScopeDetectors.get(Scope.OTHER); if (checks != null) { OtherFileVisitor visitor = new OtherFileVisitor(checks); visitor.scan(this, project, main); } } if (mCanceled) { return; } if (project == main && mScope.contains(Scope.PROGUARD_FILE)) { checkProGuard(project, main); } } private void checkProGuard(Project project, Project main) { List<Detector> detectors = mScopeDetectors.get(Scope.PROGUARD_FILE); if (detectors != null) { Project p = main != null ? main : project; List<File> files = new ArrayList<File>(); String paths = p.getProguardPath(); if (paths != null) { Splitter splitter = Splitter.on(CharMatcher.anyOf(":;")); //$NON-NLS-1$ for (String path : splitter.split(paths)) { if (path.contains("${")) { //$NON-NLS-1$ // Don't analyze the global/user proguard files continue; } File file = new File(path); if (!file.isAbsolute()) { file = new File(project.getDir(), path); } if (file.exists()) { files.add(file); } } } if (files.isEmpty()) { File file = new File(project.getDir(), OLD_PROGUARD_FILE); if (file.exists()) { files.add(file); } file = new File(project.getDir(), FN_PROJECT_PROGUARD_FILE); if (file.exists()) { files.add(file); } } for (File file : files) { Context context = new Context(this, project, main, file); fireEvent(EventType.SCANNING_FILE, context); for (Detector detector : detectors) { if (detector.appliesTo(context, file)) { detector.beforeCheckFile(context); detector.run(context); detector.afterCheckFile(context); } } } } } /** True if execution has been canceled */ boolean isCanceled() { return mCanceled; } /** * Map from VM class name to corresponding super class VM name, if available. * This map is typically null except <b>during</b> class processing. */ private Map<String, String> mSuperClassMap; /** * Returns the super class for the given class name, * which should be in VM format (e.g. java/lang/Integer, not java.lang.Integer). * If the super class is not known, returns null. This can happen if * the given class is not a known class according to the project or its * libraries, for example because it refers to one of the core libraries which * are not analyzed by lint. * * @param name the fully qualified class name * @return the corresponding super class name (in VM format), or null if not known */ @Nullable public String getSuperClass(@NonNull String name) { if (mSuperClassMap == null) { throw new IllegalStateException("Only callable during ClassScanner#checkClass"); } assert name.indexOf('.') == -1 : "Use VM signatures, e.g. java/lang/Integer"; String superClass = mSuperClassMap.get(name); if (superClass == null && mCurrentProject != null) { if ("java/lang/Object".equals(name)) { //$NON-NLS-1$ return null; } superClass = mClient.getSuperClass(mCurrentProject, name); if (superClass != null) { mSuperClassMap.put(name, superClass); } } return superClass; } /** * Returns true if the given class is a subclass of the given super class. * * @param classNode the class to check whether it is a subclass of the given * super class name * @param superClassName the fully qualified super class name (in VM format, * e.g. java/lang/Integer, not java.lang.Integer. * @return true if the given class is a subclass of the given super class */ public boolean isSubclassOf(@NonNull ClassNode classNode, @NonNull String superClassName) { if (superClassName.equals(classNode.superName)) { return true; } if (mCurrentProject != null) { Boolean isSub = mClient.isSubclassOf(mCurrentProject, classNode.name, superClassName); if (isSub != null) { return isSub.booleanValue(); } } String className = classNode.name; while (className != null) { if (className.equals(superClassName)) { return true; } className = getSuperClass(className); } return false; } @Nullable private static List<Detector> union(@Nullable List<Detector> list1, @Nullable List<Detector> list2) { if (list1 == null) { return list2; } else if (list2 == null) { return list1; } else { // Use set to pick out unique detectors, since it's possible for there to be overlap, // e.g. the DuplicateIdDetector registers both a cross-resource issue and a // single-file issue, so it shows up on both scope lists: Set<Detector> set = new HashSet<Detector>(list1.size() + list2.size()); if (list1 != null) { set.addAll(list1); } if (list2 != null) { set.addAll(list2); } return new ArrayList<Detector>(set); } } /** Check the classes in this project (and if applicable, in any library projects */ private void checkClasses(Project project, Project main) { List<File> files = project.getSubset(); if (files != null) { checkIndividualClassFiles(project, main, files); return; } // We need to read in all the classes up front such that we can initialize // the parent chains (such that for example for a virtual dispatch, we can // also check the super classes). List<File> libraries = project.getJavaLibraries(); List<ClassEntry> libraryEntries; if (!libraries.isEmpty()) { libraryEntries = new ArrayList<ClassEntry>(64); findClasses(libraryEntries, libraries); Collections.sort(libraryEntries); } else { libraryEntries = Collections.emptyList(); } List<File> classFolders = project.getJavaClassFolders(); List<ClassEntry> classEntries; if (classFolders.isEmpty()) { String message = String.format("No .class files were found in project \"%1$s\", " + "so none of the classfile based checks could be run. " + "Does the project need to be built first?", project.getName()); Location location = Location.create(project.getDir()); mClient.report(new Context(this, project, main, project.getDir()), IssueRegistry.LINT_ERROR, project.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), location, message, null); classEntries = Collections.emptyList(); } else { classEntries = new ArrayList<ClassEntry>(64); findClasses(classEntries, classFolders); Collections.sort(classEntries); } if (getPhase() == 1) { mSuperClassMap = getSuperMap(libraryEntries, classEntries); } // Actually run the detectors. Libraries should be called before the // main classes. runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main); if (mCanceled) { return; } runClassDetectors(Scope.CLASS_FILE, classEntries, project, main); runClassDetectors(Scope.ALL_CLASS_FILES, classEntries, project, main); } private void checkIndividualClassFiles(@NonNull Project project, @Nullable Project main, @NonNull List<File> files) { List<ClassEntry> entries = new ArrayList<ClassEntry>(files.size()); List<File> classFolders = project.getJavaClassFolders(); if (!classFolders.isEmpty()) { for (File file : files) { String path = file.getPath(); if (file.isFile() && path.endsWith(DOT_CLASS)) { try { byte[] bytes = mClient.readBytes(file); if (bytes != null) { for (File dir : classFolders) { if (path.startsWith(dir.getPath())) { entries.add(new ClassEntry(file, null /* jarFile*/, dir, bytes)); break; } } } } catch (IOException e) { mClient.log(e, null); continue; } if (mCanceled) { return; } } } if (!entries.isEmpty()) { Collections.sort(entries); // No superclass info available on individual lint runs, unless // the client can provide it mSuperClassMap = Maps.newHashMap(); runClassDetectors(Scope.CLASS_FILE, entries, project, main); } } } /** * Stack of {@link ClassNode} nodes for outer classes of the currently * processed class, including that class itself. Populated by * {@link #runClassDetectors(Scope, List, Project, Project)} and used by * {@link #getOuterClassNode(ClassNode)} */ private Deque<ClassNode> mOuterClasses; private void runClassDetectors(Scope scope, List<ClassEntry> entries, Project project, Project main) { if (mScope.contains(scope)) { List<Detector> classDetectors = mScopeDetectors.get(scope); if (classDetectors != null && !classDetectors.isEmpty() && !entries.isEmpty()) { AsmVisitor visitor = new AsmVisitor(mClient, classDetectors); String sourceContents = null; String sourceName = ""; mOuterClasses = new ArrayDeque<ClassNode>(); for (ClassEntry entry : entries) { ClassReader reader; ClassNode classNode; try { reader = new ClassReader(entry.bytes); classNode = new ClassNode(); reader.accept(classNode, 0 /* flags */); } catch (Throwable t) { mClient.log(null, "Error processing %1$s: broken class file?", entry.path()); continue; } ClassNode peek; while ((peek = mOuterClasses.peek()) != null) { if (classNode.name.startsWith(peek.name)) { break; } else { mOuterClasses.pop(); } } mOuterClasses.push(classNode); if (isSuppressed(null, classNode)) { // Class was annotated with suppress all -- no need to look any further continue; } if (sourceContents != null) { // Attempt to reuse the source buffer if initialized // This means making sure that the source files // foo/bar/MyClass and foo/bar/MyClass$Bar // and foo/bar/MyClass$3 and foo/bar/MyClass$3$1 have the same prefix. String newName = classNode.name; int newRootLength = newName.indexOf('$'); if (newRootLength == -1) { newRootLength = newName.length(); } int oldRootLength = sourceName.indexOf('$'); if (oldRootLength == -1) { oldRootLength = sourceName.length(); } if (newRootLength != oldRootLength || !sourceName.regionMatches(0, newName, 0, newRootLength)) { sourceContents = null; } } ClassContext context = new ClassContext(this, project, main, entry.file, entry.jarFile, entry.binDir, entry.bytes, classNode, scope == Scope.JAVA_LIBRARIES /*fromLibrary*/, sourceContents); try { visitor.runClassDetectors(context); } catch (Exception e) { mClient.log(e, null); } if (mCanceled) { return; } sourceContents = context.getSourceContents(false/*read*/); sourceName = classNode.name; } mOuterClasses = null; } } } /** Returns the outer class node of the given class node * @param classNode the inner class node * @return the outer class node */ public ClassNode getOuterClassNode(@NonNull ClassNode classNode) { String outerName = classNode.outerClass; Iterator<ClassNode> iterator = mOuterClasses.iterator(); while (iterator.hasNext()) { ClassNode node = iterator.next(); if (outerName != null) { if (node.name.equals(outerName)) { return node; } } else if (node == classNode) { return iterator.hasNext() ? iterator.next() : null; } } return null; } private Map<String, String> getSuperMap(List<ClassEntry> libraryEntries, List<ClassEntry> classEntries) { int size = libraryEntries.size() + classEntries.size(); Map<String, String> map = new HashMap<String, String>(size); SuperclassVisitor visitor = new SuperclassVisitor(map); addSuperClasses(visitor, libraryEntries); addSuperClasses(visitor, classEntries); return map; } private void addSuperClasses(SuperclassVisitor visitor, List<ClassEntry> entries) { for (ClassEntry entry : entries) { try { ClassReader reader = new ClassReader(entry.bytes); int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES; reader.accept(visitor, flags); } catch (Throwable t) { mClient.log(null, "Error processing %1$s: broken class file?", entry.path()); } } } /** * Returns the {@link ClassNode} corresponding to the given type, if possible, or null * * @param type the fully qualified type, using JVM signatures (/ and $, not . as path * separators) * @param flags the ASM flags to pass to the {@link ClassReader}, normally 0 but can * for example be {@link ClassReader#SKIP_CODE} and/oor * {@link ClassReader#SKIP_DEBUG} * @return the class node for the type, or null */ @Nullable public ClassNode findClass(@NonNull ClassContext context, @NonNull String type, int flags) { String relative = type.replace('/', File.separatorChar) + DOT_CLASS; File classFile = findClassFile(context.getProject(), relative); if (classFile != null) { if (classFile.getPath().endsWith(DOT_JAR)) { // TODO: Handle .jar files return null; } try { byte[] bytes = mClient.readBytes(classFile); ClassReader reader = new ClassReader(bytes); ClassNode classNode = new ClassNode(); reader.accept(classNode, flags); return classNode; } catch (Throwable t) { mClient.log(null, "Error processing %1$s: broken class file?", classFile.getPath()); } } return null; } @Nullable private File findClassFile(@NonNull Project project, String relativePath) { for (File root : mClient.getJavaClassFolders(project)) { File path = new File(root, relativePath); if (path.exists()) { return path; } } // Search in the libraries for (File root : mClient.getJavaLibraries(project)) { // TODO: Handle .jar files! //if (root.getPath().endsWith(DOT_JAR)) { //} File path = new File(root, relativePath); if (path.exists()) { return path; } } // Search dependent projects for (Project library : project.getDirectLibraries()) { File path = findClassFile(library, relativePath); if (path != null) { return path; } } return null; } /** Visitor skimming classes and initializing a map of super classes */ private static class SuperclassVisitor extends ClassVisitor { private final Map<String, String> mMap; public SuperclassVisitor(Map<String, String> map) { super(ASM4); mMap = map; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (superName != null) { mMap.put(name, superName); } } } private void findClasses(@NonNull List<ClassEntry> entries, @NonNull List<File> classPath) { for (File classPathEntry : classPath) { if (classPathEntry.getName().endsWith(DOT_JAR)) { File jarFile = classPathEntry; if (!jarFile.exists()) { continue; } ZipInputStream zis = null; try { FileInputStream fis = new FileInputStream(jarFile); zis = new ZipInputStream(fis); ZipEntry entry = zis.getNextEntry(); while (entry != null) { String name = entry.getName(); if (name.endsWith(DOT_CLASS)) { try { byte[] bytes = ByteStreams.toByteArray(zis); if (bytes != null) { File file = new File(entry.getName()); entries.add(new ClassEntry(file, jarFile, jarFile, bytes)); } } catch (Exception e) { mClient.log(e, null); continue; } } if (mCanceled) { return; } entry = zis.getNextEntry(); } } catch (IOException e) { mClient.log(e, "Could not read jar file contents from %1$s", jarFile); } finally { Closeables.closeQuietly(zis); } continue; } else if (classPathEntry.isDirectory()) { File binDir = classPathEntry; List<File> classFiles = new ArrayList<File>(); addClassFiles(binDir, classFiles); for (File file : classFiles) { try { byte[] bytes = mClient.readBytes(file); if (bytes != null) { entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes)); } } catch (IOException e) { mClient.log(e, null); continue; } if (mCanceled) { return; } } } else { mClient.log(null, "Ignoring class path entry %1$s", classPathEntry); } } } private static void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) { // Process the resource folder File[] files = dir.listFiles(); if (files != null && files.length > 0) { for (File file : files) { if (file.isFile() && file.getName().endsWith(DOT_CLASS)) { classFiles.add(file); } else if (file.isDirectory()) { // Recurse addClassFiles(file, classFiles); } } } } private void checkJava(@NonNull Project project, @Nullable Project main, @NonNull List<File> sourceFolders, @NonNull List<Detector> checks) { IJavaParser javaParser = mClient.getJavaParser(); if (javaParser == null) { mClient.log(null, "No java parser provided to lint: not running Java checks"); return; } assert !checks.isEmpty(); // Gather all Java source files in a single pass; more efficient. List<File> sources = new ArrayList<File>(100); for (File folder : sourceFolders) { gatherJavaFiles(folder, sources); } if (!sources.isEmpty()) { JavaVisitor visitor = new JavaVisitor(javaParser, checks); for (File file : sources) { JavaContext context = new JavaContext(this, project, main, file); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); if (mCanceled) { return; } } } } private void checkIndividualJavaFiles(@NonNull Project project, @Nullable Project main, @NonNull List<Detector> checks, @NonNull List<File> files) { IJavaParser javaParser = mClient.getJavaParser(); if (javaParser == null) { mClient.log(null, "No java parser provided to lint: not running Java checks"); return; } JavaVisitor visitor = new JavaVisitor(javaParser, checks); for (File file : files) { if (file.isFile() && file.getPath().endsWith(DOT_JAVA)) { JavaContext context = new JavaContext(this, project, main, file); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); if (mCanceled) { return; } } } } private static void gatherJavaFiles(@NonNull File dir, @NonNull List<File> result) { File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().endsWith(".java")) { //$NON-NLS-1$ result.add(file); } else if (file.isDirectory()) { gatherJavaFiles(file, result); } } } } private ResourceFolderType mCurrentFolderType; private List<ResourceXmlDetector> mCurrentXmlDetectors; private XmlVisitor mCurrentVisitor; @Nullable private XmlVisitor getVisitor(@NonNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> checks) { if (type != mCurrentFolderType) { mCurrentFolderType = type; // Determine which XML resource detectors apply to the given folder type List<ResourceXmlDetector> applicableChecks = new ArrayList<ResourceXmlDetector>(checks.size()); for (ResourceXmlDetector check : checks) { if (check.appliesTo(type)) { applicableChecks.add(check); } } // If the list of detectors hasn't changed, then just use the current visitor! if (mCurrentXmlDetectors != null && mCurrentXmlDetectors.equals(applicableChecks)) { return mCurrentVisitor; } if (applicableChecks.isEmpty()) { mCurrentVisitor = null; return null; } IDomParser parser = mClient.getDomParser(); if (parser != null) { mCurrentVisitor = new XmlVisitor(parser, applicableChecks); } } return mCurrentVisitor; } private void checkResFolder(@NonNull Project project, @Nullable Project main, @NonNull File res, @NonNull List<ResourceXmlDetector> checks) { assert res.isDirectory(); File[] resourceDirs = res.listFiles(); if (resourceDirs == null) { return; } // Sort alphabetically such that we can process related folder types at the // same time Arrays.sort(resourceDirs); for (File dir : resourceDirs) { ResourceFolderType type = ResourceFolderType.getFolderType(dir.getName()); if (type != null) { checkResourceFolder(project, main, dir, type, checks); } if (mCanceled) { return; } } } private void checkResourceFolder(@NonNull Project project, @Nullable Project main, @NonNull File dir, @NonNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> checks) { // Process the resource folder File[] xmlFiles = dir.listFiles(); if (xmlFiles != null && xmlFiles.length > 0) { XmlVisitor visitor = getVisitor(type, checks); if (visitor != null) { // if not, there are no applicable rules in this folder // Process files in alphabetical order, to ensure stable output // (for example for the duplicate resource detector) Arrays.sort(xmlFiles); for (File file : xmlFiles) { if (LintUtils.isXmlFile(file)) { XmlContext context = new XmlContext(this, project, main, file, type); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); if (mCanceled) { return; } } } } } } /** Checks individual resources */ private void checkIndividualResources(@NonNull Project project, @Nullable Project main, @NonNull List<ResourceXmlDetector> xmlDetectors, @NonNull List<File> files) { for (File file : files) { if (file.isDirectory()) { // Is it a resource folder? ResourceFolderType type = ResourceFolderType.getFolderType(file.getName()); if (type != null && new File(file.getParentFile(), RES_FOLDER).exists()) { // Yes. checkResourceFolder(project, main, file, type, xmlDetectors); } else if (file.getName().equals(RES_FOLDER)) { // Is it the res folder? // Yes checkResFolder(project, main, file, xmlDetectors); } else { mClient.log(null, "Unexpected folder %1$s; should be project, " + "\"res\" folder or resource folder", file.getPath()); continue; } } else if (file.isFile() && LintUtils.isXmlFile(file)) { // Yes, find out its resource type String folderName = file.getParentFile().getName(); ResourceFolderType type = ResourceFolderType.getFolderType(folderName); if (type != null) { XmlVisitor visitor = getVisitor(type, xmlDetectors); if (visitor != null) { XmlContext context = new XmlContext(this, project, main, file, type); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); } } } } } /** * Adds a listener to be notified of lint progress * * @param listener the listener to be added */ public void addLintListener(@NonNull LintListener listener) { if (mListeners == null) { mListeners = new ArrayList<LintListener>(1); } mListeners.add(listener); } /** * Removes a listener such that it is no longer notified of progress * * @param listener the listener to be removed */ public void removeLintListener(@NonNull LintListener listener) { mListeners.remove(listener); if (mListeners.isEmpty()) { mListeners = null; } } /** Notifies listeners, if any, that the given event has occurred */ private void fireEvent(@NonNull LintListener.EventType type, @Nullable Context context) { if (mListeners != null) { for (LintListener listener : mListeners) { listener.update(this, type, context); } } } /** * Wrapper around the lint client. This sits in the middle between a * detector calling for example {@link LintClient#report} and * the actual embedding tool, and performs filtering etc such that detectors * and lint clients don't have to make sure they check for ignored issues or * filtered out warnings. */ private class LintClientWrapper extends LintClient { @NonNull private final LintClient mDelegate; public LintClientWrapper(@NonNull LintClient delegate) { mDelegate = delegate; } @Override public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity severity, @Nullable Location location, @NonNull String message, @Nullable Object data) { assert mCurrentProject != null; if (!mCurrentProject.getReportIssues()) { return; } Configuration configuration = context.getConfiguration(); if (!configuration.isEnabled(issue)) { if (issue != IssueRegistry.PARSER_ERROR && issue != IssueRegistry.LINT_ERROR) { mDelegate.log(null, "Incorrect detector reported disabled issue %1$s", issue.toString()); } return; } if (configuration.isIgnored(context, issue, location, message, data)) { return; } if (severity == Severity.IGNORE) { return; } mDelegate.report(context, issue, severity, location, message, data); } // Everything else just delegates to the embedding lint client @Override @NonNull public Configuration getConfiguration(@NonNull Project project) { return mDelegate.getConfiguration(project); } @Override public void log(@NonNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args) { mDelegate.log(exception, format, args); } @Override @NonNull public String readFile(@NonNull File file) { return mDelegate.readFile(file); } @Override @NonNull public byte[] readBytes(@NonNull File file) throws IOException { return mDelegate.readBytes(file); } @Override @NonNull public List<File> getJavaSourceFolders(@NonNull Project project) { return mDelegate.getJavaSourceFolders(project); } @Override @NonNull public List<File> getJavaClassFolders(@NonNull Project project) { return mDelegate.getJavaClassFolders(project); } @NonNull @Override public List<File> getJavaLibraries(@NonNull Project project) { return mDelegate.getJavaLibraries(project); } @Override @NonNull public List<File> getResourceFolders(@NonNull Project project) { return mDelegate.getResourceFolders(project); } @Override @Nullable public IDomParser getDomParser() { return mDelegate.getDomParser(); } @Override @NonNull public Class<? extends Detector> replaceDetector(@NonNull Class<? extends Detector> detectorClass) { return mDelegate.replaceDetector(detectorClass); } @Override @NonNull public SdkInfo getSdkInfo(@NonNull Project project) { return mDelegate.getSdkInfo(project); } @Override @NonNull public Project getProject(@NonNull File dir, @NonNull File referenceDir) { return mDelegate.getProject(dir, referenceDir); } @Override @Nullable public IJavaParser getJavaParser() { return mDelegate.getJavaParser(); } @Override public File findResource(@NonNull String relativePath) { return mDelegate.findResource(relativePath); } @Override @Nullable public File getCacheDir(boolean create) { return mDelegate.getCacheDir(create); } @Override @NonNull protected ClassPathInfo getClassPath(@NonNull Project project) { return mDelegate.getClassPath(project); } @Override public void log(@Nullable Throwable exception, @Nullable String format, @Nullable Object... args) { mDelegate.log(exception, format, args); } @Override @Nullable public File getSdkHome() { return mDelegate.getSdkHome(); } @Override @NonNull public IAndroidTarget[] getTargets() { return mDelegate.getTargets(); } @Override public int getHighestKnownApiLevel() { return mDelegate.getHighestKnownApiLevel(); } @Override @Nullable public String getSuperClass(@NonNull Project project, @NonNull String name) { return mDelegate.getSuperClass(project, name); } @Override @Nullable public Boolean isSubclassOf(@NonNull Project project, @NonNull String name, @NonNull String superClassName) { return mDelegate.isSubclassOf(project, name, superClassName); } @Override @NonNull public String getProjectName(@NonNull Project project) { return mDelegate.getProjectName(project); } @Override public boolean isGradleProject(Project project) { return mDelegate.isGradleProject(project); } @NonNull @Override protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { return mDelegate.createProject(dir, referenceDir); } } /** * Requests another pass through the data for the given detector. This is * typically done when a detector needs to do more expensive computation, * but it only wants to do this once it <b>knows</b> that an error is * present, or once it knows more specifically what to check for. * * @param detector the detector that should be included in the next pass. * Note that the lint runner may refuse to run more than a couple * of runs. * @param scope the scope to be revisited. This must be a subset of the * current scope ({@link #getScope()}, and it is just a performance hint; * in particular, the detector should be prepared to be called on other * scopes as well (since they may have been requested by other detectors). * You can pall null to indicate "all". */ public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) { if (mRepeatingDetectors == null) { mRepeatingDetectors = new ArrayList<Detector>(); } mRepeatingDetectors.add(detector); if (scope != null) { if (mRepeatScope == null) { mRepeatScope = scope; } else { mRepeatScope = EnumSet.copyOf(mRepeatScope); mRepeatScope.addAll(scope); } } else { mRepeatScope = Scope.ALL; } } // 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. /** * Returns whether the given issue is suppressed in the given method. * * @param issue the issue to be checked, or null to just check for "all" * @param classNode the class containing the issue * @param method the method containing the issue * @param instruction the instruction within the method, if any * @return true if there is a suppress annotation covering the specific * issue on this method */ public boolean isSuppressed(@Nullable Issue issue, @NonNull ClassNode classNode, @NonNull MethodNode method, @Nullable AbstractInsnNode instruction) { if (method.invisibleAnnotations != null) { @SuppressWarnings("unchecked") List<AnnotationNode> annotations = method.invisibleAnnotations; return isSuppressed(issue, annotations); } // Initializations of fields end up placed in generated methods (<init> // for members and <clinit> for static fields). if (instruction != null && method.name.charAt(0) == '<') { AbstractInsnNode next = LintUtils.getNextInstruction(instruction); if (next != null && next.getType() == AbstractInsnNode.FIELD_INSN) { FieldInsnNode fieldRef = (FieldInsnNode) next; FieldNode field = findField(classNode, fieldRef.owner, fieldRef.name); if (field != null && isSuppressed(issue, field)) { return true; } } else if (classNode.outerClass != null && classNode.outerMethod == null && isAnonymousClass(classNode)) { if (isSuppressed(issue, classNode)) { return true; } } } return false; } @Nullable private static MethodInsnNode findConstructorInvocation(@NonNull MethodNode method, @NonNull String className) { InsnList nodes = ((MethodNode) method).instructions; for (int i = 0, n = nodes.size(); i < n; i++) { AbstractInsnNode instruction = nodes.get(i); if (instruction.getOpcode() == Opcodes.INVOKESPECIAL) { MethodInsnNode call = (MethodInsnNode) instruction; if (className.equals(call.owner)) { return call; } } } return null; } @Nullable private FieldNode findField(@NonNull ClassNode classNode, @NonNull String owner, @NonNull String name) { ClassNode current = classNode; while (current != null) { if (owner.equals(current.name)) { @SuppressWarnings("rawtypes") // ASM API List fieldList = current.fields; for (Object f : fieldList) { FieldNode field = (FieldNode) f; if (field.name.equals(name)) { return field; } } return null; } current = getOuterClassNode(current); } return null; } @Nullable private MethodNode findMethod(@NonNull ClassNode classNode, @NonNull String name, boolean includeInherited) { ClassNode current = classNode; while (current != null) { @SuppressWarnings("rawtypes") // ASM API List methodList = current.methods; for (Object f : methodList) { MethodNode method = (MethodNode) f; if (method.name.equals(name)) { return method; } } if (includeInherited) { current = getOuterClassNode(current); } else { break; } } return null; } /** * Returns whether the given issue is suppressed for the given field. * * @param issue the issue to be checked, or null to just check for "all" * @param field the field potentially annotated with a suppress annotation * @return true if there is a suppress annotation covering the specific * issue on this field */ public boolean isSuppressed(@Nullable Issue issue, @NonNull FieldNode field) { if (field.invisibleAnnotations != null) { @SuppressWarnings("unchecked") List<AnnotationNode> annotations = field.invisibleAnnotations; return isSuppressed(issue, annotations); } return false; } /** * Returns whether the given issue is suppressed in the given class. * * @param issue the issue to be checked, or null to just check for "all" * @param classNode the class containing the issue * @return true if there is a suppress annotation covering the specific * issue in this class */ public boolean isSuppressed(@Nullable Issue issue, @NonNull ClassNode classNode) { if (classNode.invisibleAnnotations != null) { @SuppressWarnings("unchecked") List<AnnotationNode> annotations = classNode.invisibleAnnotations; return isSuppressed(issue, annotations); } if (classNode.outerClass != null && classNode.outerMethod == null && isAnonymousClass(classNode)) { ClassNode outer = getOuterClassNode(classNode); if (outer != null) { MethodNode m = findMethod(outer, CONSTRUCTOR_NAME, false); if (m != null) { MethodInsnNode call = findConstructorInvocation(m, classNode.name); if (call != null) { if (isSuppressed(issue, outer, m, call)) { return true; } } } m = findMethod(outer, CLASS_CONSTRUCTOR, false); if (m != null) { MethodInsnNode call = findConstructorInvocation(m, classNode.name); if (call != null) { if (isSuppressed(issue, outer, m, call)) { return true; } } } } } return false; } private static boolean isSuppressed(@Nullable Issue issue, List<AnnotationNode> annotations) { for (AnnotationNode annotation : annotations) { String desc = annotation.desc; // We could obey @SuppressWarnings("all") too, but no need to look for it // because that annotation only has source retention. if (desc.endsWith(SUPPRESS_LINT_VMSIG)) { if (annotation.values != null) { for (int i = 0, n = annotation.values.size(); i < n; i += 2) { String key = (String) annotation.values.get(i); if (key.equals("value")) { //$NON-NLS-1$ Object value = annotation.values.get(i + 1); if (value instanceof String) { String id = (String) value; if (id.equalsIgnoreCase(SUPPRESS_ALL) || issue != null && id.equalsIgnoreCase(issue.getId())) { return true; } } else if (value instanceof List) { @SuppressWarnings("rawtypes") List list = (List) value; for (Object v : list) { if (v instanceof String) { String id = (String) v; if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null && id.equalsIgnoreCase(issue.getId()))) { return true; } } } } } } } } } return false; } /** * Returns whether the given issue is suppressed in the given parse tree node. * * @param issue the issue to be checked, or null to just check for "all" * @param scope the AST node containing the issue * @return true if there is a suppress annotation covering the specific * issue in this class */ public boolean isSuppressed(@NonNull Issue issue, @Nullable Node scope) { while (scope != null) { Class<? extends Node> type = scope.getClass(); // The Lombok AST uses a flat hierarchy of node type implementation classes // so no need to do instanceof stuff here. if (type == VariableDefinition.class) { // Variable VariableDefinition declaration = (VariableDefinition) scope; if (isSuppressed(issue, declaration.astModifiers())) { return true; } } else if (type == MethodDeclaration.class) { // Method // Look for annotations on the method MethodDeclaration declaration = (MethodDeclaration) scope; if (isSuppressed(issue, declaration.astModifiers())) { return true; } } else if (type == ConstructorDeclaration.class) { // Constructor // Look for annotations on the method ConstructorDeclaration declaration = (ConstructorDeclaration) scope; if (isSuppressed(issue, declaration.astModifiers())) { return true; } } else if (type == ClassDeclaration.class) { // Class ClassDeclaration declaration = (ClassDeclaration) scope; if (isSuppressed(issue, declaration.astModifiers())) { return true; } } scope = scope.getParent(); } return false; } /** * Returns true if the given AST modifier has a suppress annotation for the * given issue (which can be null to check for the "all" annotation) * * @param issue the issue to be checked * @param modifiers the modifier to check * @return true if the issue or all issues should be suppressed for this * modifier */ private static boolean isSuppressed(@Nullable Issue issue, @Nullable Modifiers modifiers) { if (modifiers == null) { return false; } StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations(); if (annotations == null) { return false; } Iterator<Annotation> iterator = annotations.iterator(); while (iterator.hasNext()) { Annotation annotation = iterator.next(); TypeReference t = annotation.astAnnotationTypeReference(); String typeName = t.getTypeName(); if (typeName.endsWith(SUPPRESS_LINT) || typeName.endsWith("SuppressWarnings")) { //$NON-NLS-1$ StrictListAccessor<AnnotationElement, Annotation> values = annotation.astElements(); if (values != null) { Iterator<AnnotationElement> valueIterator = values.iterator(); while (valueIterator.hasNext()) { AnnotationElement element = valueIterator.next(); AnnotationValue valueNode = element.astValue(); if (valueNode == null) { continue; } if (valueNode instanceof StringLiteral) { StringLiteral literal = (StringLiteral) valueNode; String value = literal.astValue(); if (value.equalsIgnoreCase(SUPPRESS_ALL) || issue != null && issue.getId().equalsIgnoreCase(value)) { return true; } } else if (valueNode instanceof ArrayInitializer) { ArrayInitializer array = (ArrayInitializer) valueNode; StrictListAccessor<Expression, ArrayInitializer> expressions = array.astExpressions(); if (expressions == null) { continue; } Iterator<Expression> arrayIterator = expressions.iterator(); while (arrayIterator.hasNext()) { Expression arrayElement = arrayIterator.next(); if (arrayElement instanceof StringLiteral) { String value = ((StringLiteral) arrayElement).astValue(); if (value.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null && issue.getId().equalsIgnoreCase(value))) { return true; } } } } } } } } return false; } /** * Returns whether the given issue is suppressed in the given XML DOM node. * * @param issue the issue to be checked, or null to just check for "all" * @param node the DOM node containing the issue * @return true if there is a suppress annotation covering the specific * issue in this class */ public boolean isSuppressed(@NonNull Issue issue, @Nullable org.w3c.dom.Node node) { if (node instanceof Attr) { node = ((Attr) node).getOwnerElement(); } while (node != null) { if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { Element element = (Element) node; if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) { String ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE); if (ignore.indexOf(',') == -1) { if (ignore.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null && issue.getId().equalsIgnoreCase(ignore))) { return true; } } else { for (String id : ignore.split(",")) { //$NON-NLS-1$ if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null && issue.getId().equalsIgnoreCase(id))) { return true; } } } } } node = node.getParentNode(); } return false; } /** A pending class to be analyzed by {@link #checkClasses} */ @VisibleForTesting static class ClassEntry implements Comparable<ClassEntry> { public final File file; public final File jarFile; public final File binDir; public final byte[] bytes; public ClassEntry(File file, File jarFile, File binDir, byte[] bytes) { super(); this.file = file; this.jarFile = jarFile; this.binDir = binDir; this.bytes = bytes; } public String path() { if (jarFile != null) { return jarFile.getPath() + ':' + file.getPath(); } else { return file.getPath(); } } @Override public int compareTo(ClassEntry other) { String p1 = file.getPath(); String p2 = other.file.getPath(); int m1 = p1.length(); int m2 = p2.length(); int m = Math.min(m1, m2); for (int i = 0; i < m; i++) { char c1 = p1.charAt(i); char c2 = p2.charAt(i); if (c1 != c2) { // Sort Foo$Bar.class *after* Foo.class, even though $ < . if (c1 == '.' && c2 == '$') { return -1; } if (c1 == '$' && c2 == '.') { return 1; } return c1 - c2; } } return (m == m1) ? -1 : 1; } @Override public String toString() { return file.getPath(); } } }