blob: b30cdb8585d8f3bb0fc11fed1cb2b9a1da248d6c [file] [log] [blame]
/*
* 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;
}
}