/* * 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.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.FD_GRADLE_WRAPPER; import static com.android.SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES; import static com.android.SdkConstants.FN_LOCAL_PROPERTIES; 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.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER; import static com.android.tools.lint.detector.api.LintUtils.isAnonymousClass; import static java.io.File.separator; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.res2.ResourceItem; import com.android.resources.ResourceFolderType; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.repository.local.LocalSdk; 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.ResourceContext; 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.TextFormat; import com.android.tools.lint.detector.api.XmlContext; import com.google.common.annotations.Beta; import com.google.common.base.Objects; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import org.objectweb.asm.ClassReader; 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.IOException; import java.net.URL; import java.net.URLConnection; 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.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.ast.Annotation; import lombok.ast.AnnotationElement; import lombok.ast.AnnotationValue; import lombok.ast.ArrayInitializer; 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.TypeDeclaration; 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 + ';'; /** Prefix used by the comment suppress mechanism in Studio/IntelliJ */ private static final String STUDIO_ID_PREFIX = "AndroidLint"; private final LintClient mClient; private LintRequest mRequest; private 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; } /** * Sets the scope for the lint job * * @param scope the scope to use */ public void setScope(@NonNull EnumSet<Scope> scope) { mScope = scope; } /** * 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(); assert mScope == null || !mScope.contains(Scope.ALL_RESOURCE_FILES) || mScope.contains(Scope.RESOURCE_FILE); Collection<Project> projects; try { projects = mRequest.getProjects(); if (projects == null) { projects = computeProjects(mRequest.getFiles()); } } catch (CircularDependencyException e) { mCurrentProject = e.getProject(); if (mCurrentProject != null) { Location location = e.getLocation(); File file = location != null ? location.getFile() : mCurrentProject.getDir(); Context context = new Context(this, mCurrentProject, null, file); context.report(IssueRegistry.LINT_ERROR, e.getLocation(), e.getMessage()); mCurrentProject = null; } return; } if (projects.isEmpty()) { mClient.log(null, "No projects found for %1$s", mRequest.getFiles().toString()); return; } if (mCanceled) { return; } registerCustomDetectors(projects); if (mScope == null) { mScope = Scope.infer(projects); } fireEvent(EventType.STARTING, null); for (Project project : projects) { mPhase = 1; Project main = mRequest.getMainProject(project); // The set of available detectors varies between projects computeDetectors(project); if (mApplicableDetectors.isEmpty()) { // No detectors enabled in this project: skip it continue; } checkProject(project, main); if (mCanceled) { break; } runExtraPhases(project, main); } fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null); } @Nullable private Set<Issue> myCustomIssues; /** * Returns true if the given issue is an issue that was loaded as a custom rule * (e.g. a 3rd-party library provided the detector, it's not built in) * * @param issue the issue to be looked up * @return true if this is a custom (non-builtin) check */ public boolean isCustomIssue(@NonNull Issue issue) { return myCustomIssues != null && myCustomIssues.contains(issue); } private void registerCustomDetectors(Collection<Project> projects) { // Look at the various projects, and if any of them provide a custom // lint jar, "add" them (this will replace the issue registry with // a CompositeIssueRegistry containing the original issue registry // plus JarFileIssueRegistry instances for each lint jar Set<File> jarFiles = Sets.newHashSet(); for (Project project : projects) { jarFiles.addAll(mClient.findRuleJars(project)); for (Project library : project.getAllLibraries()) { jarFiles.addAll(mClient.findRuleJars(library)); } } jarFiles.addAll(mClient.findGlobalRuleJars()); if (!jarFiles.isEmpty()) { List<IssueRegistry> registries = Lists.newArrayListWithExpectedSize(jarFiles.size()); registries.add(mRegistry); for (File jarFile : jarFiles) { try { IssueRegistry registry = JarFileIssueRegistry.get(mClient, jarFile); if (myCustomIssues == null) { myCustomIssues = Sets.newHashSet(); } myCustomIssues.addAll(registry.getIssues()); registries.add(registry); } catch (Throwable e) { mClient.log(e, "Could not load custom rule jar file %1$s", jarFile); } } if (registries.size() > 1) { // the first item is mRegistry itself mRegistry = new CompositeIssueRegistry(registries); } } } private void runExtraPhases(@NonNull Project project, @NonNull Project main) { // 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, main); 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; mCurrentXmlDetectors = null; mCurrentBinaryDetectors = 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(this); 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(this); 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> gradleDetectors = mScopeDetectors.get(Scope.GRADLE_FILE); if (gradleDetectors != null) { for (Detector detector : gradleDetectors) { assert detector instanceof Detector.GradleScanner : detector; } } List<Detector> otherDetectors = mScopeDetectors.get(Scope.OTHER); if (otherDetectors != null) { for (Detector detector : otherDetectors) { assert detector instanceof Detector.OtherFileScanner : detector; } } List<Detector> dirDetectors = mScopeDetectors.get(Scope.RESOURCE_FOLDER); if (dirDetectors != null) { for (Detector detector : dirDetectors) { assert detector instanceof Detector.ResourceFolderScanner : detector; } } List<Detector> binaryDetectors = mScopeDetectors.get(Scope.BINARY_RESOURCE_FILE); if (binaryDetectors != null) { for (Detector detector : binaryDetectors) { assert detector instanceof Detector.BinaryResourceScanner : 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 LinkedHashMap<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 (mClient.isProjectDirectory(file)) { registerProjectFile(fileToProject, file, file, rootDir); continue; } else { File parent = file.getParentFile(); if (parent != null) { if (mClient.isProjectDirectory(parent)) { registerProjectFile(fileToProject, file, parent, parent); continue; } else { parent = parent.getParentFile(); if (parent != null && mClient.isProjectDirectory(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 (mClient.isProjectDirectory(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 (mClient.isProjectDirectory(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, @NonNull Project main) { 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, main); if (!Scope.checkSingleFile(mScope)) { List<Project> libraries = project.getAllLibraries(); 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, main); 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", TextFormat.RAW); } mCurrentProjects = null; } private void runFileDetectors(@NonNull Project project, @Nullable Project main) { // Look up manifest information (but not for library projects) if (project.isAndroidProject()) { for (File manifestFile : project.getManifestFiles()) { XmlParser parser = mClient.getXmlParser(); if (parser != null) { XmlContext context = new XmlContext(this, project, main, manifestFile, null, parser); 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) { ResourceVisitor v = new ResourceVisitor(parser, detectors, null); 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) || mScope.contains(Scope.RESOURCE_FOLDER) || mScope.contains(Scope.BINARY_RESOURCE_FILE)) { List<Detector> dirChecks = mScopeDetectors.get(Scope.RESOURCE_FOLDER); List<Detector> binaryChecks = mScopeDetectors.get(Scope.BINARY_RESOURCE_FILE); List<Detector> checks = union(mScopeDetectors.get(Scope.RESOURCE_FILE), mScopeDetectors.get(Scope.ALL_RESOURCE_FILES)); boolean haveXmlChecks = checks != null && !checks.isEmpty(); List<ResourceXmlDetector> xmlDetectors; if (haveXmlChecks) { xmlDetectors = new ArrayList<ResourceXmlDetector>(checks.size()); for (Detector detector : checks) { if (detector instanceof ResourceXmlDetector) { xmlDetectors.add((ResourceXmlDetector) detector); } } haveXmlChecks = !xmlDetectors.isEmpty(); } else { xmlDetectors = Collections.emptyList(); } if (haveXmlChecks || dirChecks != null && !dirChecks.isEmpty() || binaryChecks != null && !binaryChecks.isEmpty()) { List<File> files = project.getSubset(); if (files != null) { checkIndividualResources(project, main, xmlDetectors, dirChecks, binaryChecks, files); } else { List<File> resourceFolders = project.getResourceFolders(); if (!resourceFolders.isEmpty()) { for (File res : resourceFolders) { checkResFolder(project, main, res, xmlDetectors, dirChecks, binaryChecks); } } } } } 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(); if (mScope.contains(Scope.TEST_SOURCES)) { List<File> testFolders = project.getTestSourceFolders(); if (!testFolders.isEmpty()) { List<File> combined = Lists.newArrayListWithExpectedSize( sourceFolders.size() + testFolders.size()); combined.addAll(sourceFolders); combined.addAll(testFolders); sourceFolders = combined; } } 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 (mCanceled) { return; } if (mScope.contains(Scope.GRADLE_FILE)) { checkBuildScripts(project, main); } if (mCanceled) { return; } 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) && project.isAndroidProject()) { checkProGuard(project, main); } if (project == main && mScope.contains(Scope.PROPERTY_FILE)) { checkProperties(project, main); } } private void checkBuildScripts(Project project, Project main) { List<Detector> detectors = mScopeDetectors.get(Scope.GRADLE_FILE); if (detectors != null) { List<File> files = project.getSubset(); if (files == null) { files = project.getGradleBuildScripts(); } 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.visitBuildScript(context, Maps.<String, Object>newHashMap()); detector.afterCheckFile(context); } } } } } private void checkProGuard(Project project, Project main) { List<Detector> detectors = mScopeDetectors.get(Scope.PROGUARD_FILE); if (detectors != null) { List<File> files = project.getProguardFiles(); 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); } } } } } private void checkProperties(Project project, Project main) { List<Detector> detectors = mScopeDetectors.get(Scope.PROPERTY_FILE); if (detectors != null) { checkPropertyFile(project, main, detectors, FN_LOCAL_PROPERTIES); checkPropertyFile(project, main, detectors, FD_GRADLE_WRAPPER + separator + FN_GRADLE_WRAPPER_PROPERTIES); } } private void checkPropertyFile(Project project, Project main, List<Detector> detectors, String relativePath) { File file = new File(project.getDir(), relativePath); if (file.exists()) { 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; } /** * 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) { return mClient.getSuperClass(mCurrentProject, name); } /** * 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; } } 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()); set.addAll(list1); 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 = ClassEntry.fromClassPath(mClient, libraries, true); 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(this).getSeverity(IssueRegistry.LINT_ERROR), location, message, TextFormat.RAW); classEntries = Collections.emptyList(); } else { classEntries = ClassEntry.fromClassPath(mClient, classFolders, true); } // 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<File> classFiles = Lists.newArrayListWithExpectedSize(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)) { classFiles.add(file); } } } List<ClassEntry> entries = ClassEntry.fromClassFiles(mClient, classFiles, classFolders, true); if (!entries.isEmpty()) { Collections.sort(entries); 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>(); ClassEntry prev = null; for (ClassEntry entry : entries) { if (prev != null && prev.compareTo(entry) == 0) { // Duplicate entries for some reason: ignore continue; } prev = entry; 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; } /** * 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; } private void checkJava( @NonNull Project project, @Nullable Project main, @NonNull List<File> sourceFolders, @NonNull List<Detector> checks) { JavaParser javaParser = mClient.getJavaParser(project); 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); List<JavaContext> contexts = Lists.newArrayListWithExpectedSize(sources.size()); for (File file : sources) { JavaContext context = new JavaContext(this, project, main, file, javaParser); contexts.add(context); } visitor.prepare(contexts); for (JavaContext context : contexts) { fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context); if (mCanceled) { return; } } visitor.dispose(); } } private void checkIndividualJavaFiles( @NonNull Project project, @Nullable Project main, @NonNull List<Detector> checks, @NonNull List<File> files) { JavaParser javaParser = mClient.getJavaParser(project); if (javaParser == null) { mClient.log(null, "No java parser provided to lint: not running Java checks"); return; } JavaVisitor visitor = new JavaVisitor(javaParser, checks); List<JavaContext> contexts = Lists.newArrayListWithExpectedSize(files.size()); for (File file : files) { if (file.isFile() && file.getPath().endsWith(DOT_JAVA)) { contexts.add(new JavaContext(this, project, main, file, javaParser)); } } if (contexts.isEmpty()) { return; } visitor.prepare(contexts); if (mCanceled) { return; } for (JavaContext context : contexts) { fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context); if (mCanceled) { return; } } visitor.dispose(); } 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 List<Detector> mCurrentBinaryDetectors; private ResourceVisitor mCurrentVisitor; @Nullable private ResourceVisitor getVisitor( @NonNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> checks, @Nullable List<Detector> binaryChecks) { if (type != mCurrentFolderType) { mCurrentFolderType = type; // Determine which XML resource detectors apply to the given folder type List<ResourceXmlDetector> applicableXmlChecks = new ArrayList<ResourceXmlDetector>(checks.size()); for (ResourceXmlDetector check : checks) { if (check.appliesTo(type)) { applicableXmlChecks.add(check); } } List<Detector> applicableBinaryChecks = null; if (binaryChecks != null) { applicableBinaryChecks = new ArrayList<Detector>(binaryChecks.size()); for (Detector check : binaryChecks) { if (check.appliesTo(type)) { applicableBinaryChecks.add(check); } } } // If the list of detectors hasn't changed, then just use the current visitor! if (mCurrentXmlDetectors != null && mCurrentXmlDetectors.equals(applicableXmlChecks) && Objects.equal(mCurrentBinaryDetectors, applicableBinaryChecks)) { return mCurrentVisitor; } mCurrentXmlDetectors = applicableXmlChecks; mCurrentBinaryDetectors = applicableBinaryChecks; if (applicableXmlChecks.isEmpty() && (applicableBinaryChecks == null || applicableBinaryChecks.isEmpty())) { mCurrentVisitor = null; return null; } XmlParser parser = mClient.getXmlParser(); if (parser != null) { mCurrentVisitor = new ResourceVisitor(parser, applicableXmlChecks, applicableBinaryChecks); } else { mCurrentVisitor = null; } } return mCurrentVisitor; } private void checkResFolder( @NonNull Project project, @Nullable Project main, @NonNull File res, @NonNull List<ResourceXmlDetector> xmlChecks, @Nullable List<Detector> dirChecks, @Nullable List<Detector> binaryChecks) { File[] resourceDirs = res.listFiles(); if (resourceDirs == null) { return; } // Sort alphabetically such that we can process related folder types at the // same time, and to have a defined behavior such that detectors can rely on // predictable ordering, e.g. layouts are seen before menus are seen before // values, etc (l < m < v). Arrays.sort(resourceDirs); for (File dir : resourceDirs) { ResourceFolderType type = ResourceFolderType.getFolderType(dir.getName()); if (type != null) { checkResourceFolder(project, main, dir, type, xmlChecks, dirChecks, binaryChecks); } if (mCanceled) { return; } } } private void checkResourceFolder( @NonNull Project project, @Nullable Project main, @NonNull File dir, @NonNull ResourceFolderType type, @NonNull List<ResourceXmlDetector> xmlChecks, @Nullable List<Detector> dirChecks, @Nullable List<Detector> binaryChecks) { // Process the resource folder if (dirChecks != null && !dirChecks.isEmpty()) { ResourceContext context = new ResourceContext(this, project, main, dir, type); String folderName = dir.getName(); fireEvent(EventType.SCANNING_FILE, context); for (Detector check : dirChecks) { if (check.appliesTo(type)) { check.beforeCheckFile(context); check.checkFolder(context, folderName); check.afterCheckFile(context); } } if (binaryChecks == null && xmlChecks.isEmpty()) { return; } } File[] files = dir.listFiles(); if (files == null || files.length <= 0) { return; } ResourceVisitor visitor = getVisitor(type, xmlChecks, binaryChecks); 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(files); for (File file : files) { if (LintUtils.isXmlFile(file)) { XmlContext context = new XmlContext(this, project, main, file, type, visitor.getParser()); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); } else if (binaryChecks != null && LintUtils.isBitmapFile(file)) { ResourceContext context = new ResourceContext(this, project, main, file, type); fireEvent(EventType.SCANNING_FILE, context); visitor.visitBinaryResource(context); } if (mCanceled) { return; } } } } /** Checks individual resources */ private void checkIndividualResources( @NonNull Project project, @Nullable Project main, @NonNull List<ResourceXmlDetector> xmlDetectors, @Nullable List<Detector> dirChecks, @Nullable List<Detector> binaryChecks, @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, dirChecks, binaryChecks); } else if (file.getName().equals(RES_FOLDER)) { // Is it the res folder? // Yes checkResFolder(project, main, file, xmlDetectors, dirChecks, binaryChecks); } else { mClient.log(null, "Unexpected folder %1$s; should be project, " + "\"res\" folder or resource folder", file.getPath()); } } 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) { ResourceVisitor visitor = getVisitor(type, xmlDetectors, binaryChecks); if (visitor != null) { XmlContext context = new XmlContext(this, project, main, file, type, visitor.getParser()); fireEvent(EventType.SCANNING_FILE, context); visitor.visitFile(context, file); } } } else if (binaryChecks != null && file.isFile() && LintUtils.isBitmapFile(file)) { // Yes, find out its resource type String folderName = file.getParentFile().getName(); ResourceFolderType type = ResourceFolderType.getFolderType(folderName); if (type != null) { ResourceVisitor visitor = getVisitor(type, xmlDetectors, binaryChecks); if (visitor != null) { ResourceContext context = new ResourceContext(this, project, main, file, type); fireEvent(EventType.SCANNING_FILE, context); visitor.visitBinaryResource(context); if (mCanceled) { return; } } } } } } /** * 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, @NonNull TextFormat format) { 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)) { return; } if (severity == Severity.IGNORE) { return; } mDelegate.report(context, issue, severity, location, message, format); } // Everything else just delegates to the embedding lint client @Override @NonNull public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) { return mDelegate.getConfiguration(project, driver); } @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 XmlParser getXmlParser() { return mDelegate.getXmlParser(); } @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); } @Nullable @Override public JavaParser getJavaParser(@Nullable Project project) { return mDelegate.getJavaParser(project); } @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(); } @Nullable @Override public LocalSdk getSdk() { return mDelegate.getSdk(); } @Nullable @Override public IAndroidTarget getCompileTarget(@NonNull Project project) { return mDelegate.getCompileTarget(project); } @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); } @NonNull @Override public List<File> findGlobalRuleJars() { return mDelegate.findGlobalRuleJars(); } @NonNull @Override public List<File> findRuleJars(@NonNull Project project) { return mDelegate.findRuleJars(project); } @Override public boolean isProjectDirectory(@NonNull File dir) { return mDelegate.isProjectDirectory(dir); } @Override public void registerProject(@NonNull File dir, @NonNull Project project) { log(Severity.WARNING, null, "Too late to register projects"); mDelegate.registerProject(dir, project); } @Override public IssueRegistry addCustomLintRules(@NonNull IssueRegistry registry) { return mDelegate.addCustomLintRules(registry); } @Override public boolean checkForSuppressComments() { return mDelegate.checkForSuppressComments(); } @Override public boolean supportsProjectResources() { return mDelegate.supportsProjectResources(); } @Nullable @Override public AbstractResourceRepository getProjectResources(Project project, boolean includeDependencies) { return mDelegate.getProjectResources(project, includeDependencies); } @NonNull @Override public Location.Handle createResourceItemHandle(@NonNull ResourceItem item) { return mDelegate.createResourceItemHandle(item); } @Nullable @Override public URLConnection openConnection(@NonNull URL url) throws IOException { return mDelegate.openConnection(url); } @Override public void closeConnection(@NonNull URLConnection connection) throws IOException { mDelegate.closeConnection(connection); } } /** * 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 = 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 */ @SuppressWarnings("MethodMayBeStatic") // API; reserve need to require driver state later 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 (matches(issue, id)) { 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 (matches(issue, id)) { return true; } } } } } } } } } return false; } private static boolean matches(@Nullable Issue issue, @NonNull String id) { if (id.equalsIgnoreCase(SUPPRESS_ALL)) { return true; } if (issue != null) { String issueId = issue.getId(); if (id.equalsIgnoreCase(issueId)) { return true; } if (id.startsWith(STUDIO_ID_PREFIX) && id.regionMatches(true, STUDIO_ID_PREFIX.length(), issueId, 0, issueId.length()) && id.substring(STUDIO_ID_PREFIX.length()).equalsIgnoreCase(issueId)) { return true; } } return false; } /** * Returns whether the given issue is suppressed in the given parse tree node. * * @param context the context for the source being scanned * @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(@Nullable JavaContext context, @NonNull Issue issue, @Nullable Node scope) { boolean checkComments = mClient.checkForSuppressComments() && context != null && context.containsCommentSuppress(); 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 (TypeDeclaration.class.isAssignableFrom(type)) { // Class, annotation, enum, interface TypeDeclaration declaration = (TypeDeclaration) scope; if (isSuppressed(issue, declaration.astModifiers())) { return true; } } if (checkComments && context.isSuppressedWithComment(scope, issue)) { 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; } for (Annotation annotation : annotations) { 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) { for (AnnotationElement element : values) { AnnotationValue valueNode = element.astValue(); if (valueNode == null) { continue; } if (valueNode instanceof StringLiteral) { StringLiteral literal = (StringLiteral) valueNode; String value = literal.astValue(); if (matches(issue, value)) { return true; } } else if (valueNode instanceof ArrayInitializer) { ArrayInitializer array = (ArrayInitializer) valueNode; StrictListAccessor<Expression, ArrayInitializer> expressions = array.astExpressions(); if (expressions == null) { continue; } for (Expression arrayElement : expressions) { if (arrayElement instanceof StringLiteral) { String value = ((StringLiteral) arrayElement).astValue(); if (matches(issue, 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(@Nullable XmlContext context, @NonNull Issue issue, @Nullable org.w3c.dom.Node node) { if (node instanceof Attr) { node = ((Attr) node).getOwnerElement(); } boolean checkComments = mClient.checkForSuppressComments() && context != null && context.containsCommentSuppress(); 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 (matches(issue, ignore)) { return true; } } else { for (String id : ignore.split(",")) { //$NON-NLS-1$ if (matches(issue, id)) { return true; } } } } else if (checkComments && context.isSuppressedWithComment(node, issue)) { return true; } } node = node.getParentNode(); } return false; } private File mCachedFolder = null; private int mCachedFolderVersion = -1; /** Pattern for version qualifiers */ private static final Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$"); //$NON-NLS-1$ /** * Returns the folder version of the given file. For example, for the file values-v14/foo.xml, * it returns 14. * * @param resourceFile the file to be checked * @return the folder version, or -1 if no specific version was specified */ public int getResourceFolderVersion(@NonNull File resourceFile) { File parent = resourceFile.getParentFile(); if (parent == null) { return -1; } if (parent.equals(mCachedFolder)) { return mCachedFolderVersion; } mCachedFolder = parent; mCachedFolderVersion = -1; for (String qualifier : QUALIFIER_SPLITTER.split(parent.getName())) { Matcher matcher = VERSION_PATTERN.matcher(qualifier); if (matcher.matches()) { String group = matcher.group(1); assert group != null; mCachedFolderVersion = Integer.parseInt(group); break; } } return mCachedFolderVersion; } }