Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.android.tools.lint.client.api.LintDriver Maven / Gradle / Ivy
/*
* 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.FQCN_SUPPRESS_LINT;
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.annotations.VisibleForTesting;
import com.android.ide.common.repository.ResourceVisibilityLookup;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.repository.api.ProgressIndicator;
import com.android.resources.ResourceFolderType;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.AndroidSdkHandler;
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.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
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 com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiAnnotationParameterList;
import com.intellij.psi.PsiArrayInitializerExpression;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiLiteral;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiModifierListOwner;
import com.intellij.psi.PsiNameValuePair;
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.AnnotationMethodDeclaration;
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;
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;
/**
* Analyzes Android projects and files
*
* NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.
*/
@Beta
public class 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 client;
private LintRequest request;
private IssueRegistry registry;
private volatile boolean canceled;
private EnumSet scope;
private List extends Detector> applicableDetectors;
private Map> scopeDetectors;
private List listeners;
private int phase;
private List repeatingDetectors;
private EnumSet repeatScope;
private Project[] currentProjects;
private Project currentProject;
private boolean abbreviating = true;
private boolean parserErrors;
private Map properties;
/** Whether we need to look for legacy (old Lombok-based Java API) detectors */
private boolean runCompatChecks = true;
private LintBaseline baseline;
/**
* 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) {
this.registry = registry;
this.client = new LintClientWrapper(client);
}
/**
* Number of fatal exceptions (internal errors, usually from ECJ) we've
* encountered; we don't log each and every one to avoid massive log spam
* in code which triggers this condition
*/
private static int exceptionCount;
/** Max number of logs to include */
private static final int MAX_REPORTED_CRASHES = 20;
/** Logs the given error produced by the various lint detectors */
public static void handleDetectorError(@NonNull Context context, @NonNull RuntimeException e) {
String simpleClassName = e.getClass().getSimpleName();
if (simpleClassName.equals("IndexNotReadyException")) {
// Attempting to access PSI during startup before indices are ready; ignore these.
// See http://b.android.com/176644 for an example.
return;
} else if (e instanceof ProcessCanceledException ||
simpleClassName.equals("ProcessCanceledException")) {
// Cancelling inspections in the IDE
context.getDriver().cancel();
return;
}
if (exceptionCount++ > MAX_REPORTED_CRASHES) {
// No need to keep spamming the user that a lot of the files
// are tripping up ECJ, they get the picture.
return;
}
StringBuilder sb = new StringBuilder(100);
sb.append("Unexpected failure during lint analysis of ");
sb.append(context.file.getName());
sb.append(" (this is a bug in lint or one of the libraries it depends on)\n");
sb.append(simpleClassName);
sb.append(':');
StackTraceElement[] stackTrace = e.getStackTrace();
int count = 0;
for (StackTraceElement frame : stackTrace) {
if (count > 0) {
sb.append("<-");
}
String className = frame.getClassName();
sb.append(className.substring(className.lastIndexOf('.') + 1));
sb.append('.').append(frame.getMethodName());
sb.append('(');
sb.append(frame.getFileName()).append(':').append(frame.getLineNumber());
sb.append(')');
count++;
// Only print the top 3-4 frames such that we can identify the bug
if (count == 4) {
break;
}
}
Throwable throwable = null; // NOT e: this makes for very noisy logs
//noinspection ConstantConditions
context.log(throwable, sb.toString());
}
/**
* For testing only: returns the number of exceptions thrown during Java AST analysis
*
* @return the number of internal errors found
*/
@VisibleForTesting
public static int getCrashCount() {
return exceptionCount;
}
/**
* For testing only: clears the crash counter
*/
@VisibleForTesting
public static void clearCrashCount() {
exceptionCount = 0;
}
/** Cancels the current lint run as soon as possible */
public void cancel() {
canceled = true;
}
/**
* Returns the scope for the lint job
*
* @return the scope, never null
*/
@NonNull
public EnumSet getScope() {
return scope;
}
/**
* Sets the scope for the lint job
*
* @param scope the scope to use
*/
public void setScope(@NonNull EnumSet scope) {
this.scope = 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 client;
}
/**
* 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 request;
}
/**
* 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 (properties == null) {
properties = Maps.newHashMap();
}
if (value == null) {
properties.remove(key);
} else {
properties.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 (properties != null) {
return properties.get(key);
}
return null;
}
@Nullable
public LintBaseline getBaseline() {
return baseline;
}
public void setBaseline(@Nullable LintBaseline baseline) {
this.baseline = baseline;
}
/**
* 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 phase;
}
/**
* Returns the current {@link IssueRegistry}.
*
* @return the current {@link IssueRegistry}
*/
@NonNull
public IssueRegistry getRegistry() {
return registry;
}
/**
* 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 (currentProjects != null) {
if (currentProjects.length == 1) {
return currentProjects[0];
}
String path = file.getPath();
for (Project project : currentProjects) {
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) {
this.abbreviating = abbreviating;
}
/**
* Returns whether lint should abbreviate output when appropriate.
*
* @return true if lint should abbreviate output, false when including everything
*/
public boolean isAbbreviating() {
return abbreviating;
}
/**
* Returns whether lint has encountered any files with fatal parser errors
* (e.g. broken source code, or even broken parsers)
*
* 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 parserErrors;
}
/**
* 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) {
parserErrors = hasErrors;
}
/**
* Returns the projects being analyzed
*
* @return the projects being analyzed
*/
@NonNull
public List getProjects() {
if (currentProjects != null) {
return Arrays.asList(currentProjects);
}
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 files, @Nullable EnumSet scope) {
analyze(new LintRequest(client, 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}.
*
* 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 {
this.request = request;
analyze();
} finally {
this.request = null;
}
}
/** Runs the driver to analyze the requested files */
private void analyze() {
canceled = false;
scope = request.getScope();
assert scope == null || !scope.contains(Scope.ALL_RESOURCE_FILES) ||
scope.contains(Scope.RESOURCE_FILE);
Collection projects;
try {
projects = request.getProjects();
if (projects == null) {
projects = computeProjects(request.getFiles());
}
} catch (CircularDependencyException e) {
currentProject = e.getProject();
if (currentProject != null) {
Location location = e.getLocation();
File file = location != null ? location.getFile() : currentProject.getDir();
Context context = new Context(this, currentProject, null, file);
context.report(IssueRegistry.LINT_ERROR, e.getLocation(), e.getMessage());
currentProject = null;
}
return;
}
if (projects.isEmpty()) {
client.log(null, "No projects found for %1$s", request.getFiles().toString());
return;
}
if (canceled) {
return;
}
registerCustomDetectors(projects);
if (scope == null) {
scope = Scope.infer(projects);
}
// See if the lint.xml file specifies a baseline and we're not in incremental mode
if (baseline == null && scope.size() > 2) {
Project lastProject = Iterables.getLast(projects);
Configuration mainConfiguration = client.getConfiguration(lastProject, this);
File baselineFile = mainConfiguration.getBaselineFile();
if (baselineFile != null) {
baseline = new LintBaseline(client, baselineFile);
}
}
fireEvent(EventType.STARTING, null);
for (Project project : projects) {
phase = 1;
Project main = request.getMainProject(project);
// The set of available detectors varies between projects
computeDetectors(project);
if (applicableDetectors.isEmpty()) {
// No detectors enabled in this project: skip it
continue;
}
checkProject(project, main);
if (canceled) {
break;
}
runExtraPhases(project, main);
}
if (baseline != null) {
Project lastProject = Iterables.getLast(projects);
Project main = request.getMainProject(lastProject);
baseline.reportBaselineIssues(this, main);
}
fireEvent(canceled ? EventType.CANCELED : EventType.COMPLETED, null);
}
@Nullable
private Set 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 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 jarFiles = Sets.newHashSet();
for (Project project : projects) {
jarFiles.addAll(client.findRuleJars(project));
for (Project library : project.getAllLibraries()) {
jarFiles.addAll(client.findRuleJars(library));
}
}
jarFiles.addAll(client.findGlobalRuleJars());
if (!jarFiles.isEmpty()) {
List registries = Lists.newArrayListWithExpectedSize(jarFiles.size());
registries.add(registry);
for (File jarFile : jarFiles) {
try {
JarFileIssueRegistry registry = JarFileIssueRegistry.get(client, jarFile);
if (registry.hasLegacyDetectors()) {
runCompatChecks = true;
}
if (myCustomIssues == null) {
myCustomIssues = Sets.newHashSet();
}
myCustomIssues.addAll(registry.getIssues());
registries.add(registry);
} catch (Throwable e) {
client.log(e, "Could not load custom rule jar file %1$s", jarFile);
}
}
if (registries.size() > 1) { // the first item is registry itself
registry = new CompositeIssueRegistry(registries);
}
}
}
private void runExtraPhases(@NonNull Project project, @NonNull Project main) {
// Did any detectors request another phase?
if (repeatingDetectors != 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 oldScope = scope;
do {
phase++;
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 (repeatScope == null) {
repeatScope = Scope.ALL;
}
scope = Scope.intersect(scope, repeatScope);
if (scope.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(repeatingDetectors, project);
if (applicableDetectors.isEmpty()) {
// No detectors enabled in this project: skip it
continue;
}
checkProject(project, main);
if (canceled) {
break;
}
} while (phase < MAX_PHASES && repeatingDetectors != null);
scope = oldScope;
}
}
private void computeRepeatingDetectors(List detectors, Project project) {
// Ensure that the current visitor is recomputed
currentFolderType = null;
currentVisitor = null;
currentXmlDetectors = null;
currentBinaryDetectors = 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 issues = registry.getIssues();
Multimap, Issue> issueMap =
ArrayListMultimap.create(issues.size(), 3);
for (Issue issue : issues) {
issueMap.put(issue.getImplementation().getDetectorClass(), issue);
}
Map, EnumSet> detectorToScope =
new HashMap<>();
Map> scopeToDetectors =
new EnumMap<>(Scope.class);
List detectorList = new ArrayList<>();
// Compute the list of detectors (narrowed down from repeatingDetectors),
// and simultaneously build up the detectorToScope map which tracks
// the scopes each detector is affected by (this is used to populate
// the scopeDetectors map which is used during iteration).
Configuration configuration = project.getConfiguration(this);
for (Detector detector : detectors) {
Class extends Detector> detectorClass = detector.getClass();
Collection 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 s = detectorToScope.get(detectorClass);
EnumSet issueScope = issue.getImplementation().getScope();
if (s == null) {
detectorToScope.put(detectorClass, issueScope);
} else if (!s.containsAll(issueScope)) {
EnumSet union = EnumSet.copyOf(s);
union.addAll(issueScope);
detectorToScope.put(detectorClass, union);
}
}
if (add) {
detectorList.add(detector);
EnumSet union = detectorToScope.get(detector.getClass());
for (Scope s : union) {
List list = scopeToDetectors.get(s);
if (list == null) {
list = new ArrayList<>();
scopeToDetectors.put(s, list);
}
list.add(detector);
}
}
}
}
applicableDetectors = detectorList;
scopeDetectors = scopeToDetectors;
repeatingDetectors = null;
repeatScope = null;
validateScopeList();
}
private void computeDetectors(@NonNull Project project) {
// Ensure that the current visitor is recomputed
currentFolderType = null;
currentVisitor = null;
Configuration configuration = project.getConfiguration(this);
scopeDetectors = new EnumMap<>(Scope.class);
applicableDetectors = registry.createDetectors(client, configuration,
scope, scopeDetectors);
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 resourceFileDetectors = scopeDetectors.get(Scope.RESOURCE_FILE);
if (resourceFileDetectors != null) {
for (Detector detector : resourceFileDetectors) {
// This is wrong; it should allow XmlScanner instead of ResourceXmlScanner!
assert detector instanceof ResourceXmlDetector : detector;
}
}
List manifestDetectors = scopeDetectors.get(Scope.MANIFEST);
if (manifestDetectors != null) {
for (Detector detector : manifestDetectors) {
assert detector instanceof Detector.XmlScanner : detector;
}
}
List javaCodeDetectors = scopeDetectors.get(Scope.ALL_JAVA_FILES);
if (javaCodeDetectors != null) {
for (Detector detector : javaCodeDetectors) {
assert detector instanceof Detector.JavaScanner ||
// TODO: Migrate all
detector instanceof Detector.JavaPsiScanner : detector;
}
}
List javaFileDetectors = scopeDetectors.get(Scope.JAVA_FILE);
if (javaFileDetectors != null) {
for (Detector detector : javaFileDetectors) {
assert detector instanceof Detector.JavaScanner ||
// TODO: Migrate all
detector instanceof Detector.JavaPsiScanner : detector;
}
}
List classDetectors = scopeDetectors.get(Scope.CLASS_FILE);
if (classDetectors != null) {
for (Detector detector : classDetectors) {
assert detector instanceof Detector.ClassScanner : detector;
}
}
List classCodeDetectors = scopeDetectors.get(Scope.ALL_CLASS_FILES);
if (classCodeDetectors != null) {
for (Detector detector : classCodeDetectors) {
assert detector instanceof Detector.ClassScanner : detector;
}
}
List gradleDetectors = scopeDetectors.get(Scope.GRADLE_FILE);
if (gradleDetectors != null) {
for (Detector detector : gradleDetectors) {
assert detector instanceof Detector.GradleScanner : detector;
}
}
List otherDetectors = scopeDetectors.get(Scope.OTHER);
if (otherDetectors != null) {
for (Detector detector : otherDetectors) {
assert detector instanceof Detector.OtherFileScanner : detector;
}
}
List dirDetectors = scopeDetectors.get(Scope.RESOURCE_FOLDER);
if (dirDetectors != null) {
for (Detector detector : dirDetectors) {
assert detector instanceof Detector.ResourceFolderScanner : detector;
}
}
List binaryDetectors = scopeDetectors.get(Scope.BINARY_RESOURCE_FILE);
if (binaryDetectors != null) {
for (Detector detector : binaryDetectors) {
assert detector instanceof Detector.BinaryResourceScanner : detector;
}
}
}
}
private void registerProjectFile(
@NonNull Map fileToProject,
@NonNull File file,
@NonNull File projectDir,
@NonNull File rootDir) {
fileToProject.put(file, client.getProject(projectDir, rootDir));
}
private Collection computeProjects(@NonNull List files) {
// Compute list of projects
Map fileToProject = new LinkedHashMap<>();
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
List absolute = new ArrayList<>(files.size());
for (File file : files) {
absolute.add(file.getAbsoluteFile());
}
// Always use absoluteFiles so that we can check the file's getParentFile()
// which is null if the file is not absolute.
files = absolute;
if (files.size() > 1) {
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 (client.isProjectDirectory(file)) {
registerProjectFile(fileToProject, file, file, rootDir);
continue;
} else {
File parent = file.getParentFile();
if (parent != null) {
if (client.isProjectDirectory(parent)) {
registerProjectFile(fileToProject, file, parent, parent);
continue;
} else {
parent = parent.getParentFile();
if (parent != null && client.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 (client.isProjectDirectory(parent)) {
registerProjectFile(fileToProject, file, parent, parent);
break;
}
parent = parent.getParentFile();
}
}
if (canceled) {
return Collections.emptySet();
}
}
for (Map.Entry 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 allProjects = fileToProject.values();
Set roots = new HashSet<>(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 projects =
new IdentityHashMap<>();
for (Project project : roots) {
projects.put(project, project);
for (Project library : project.getAllLibraries()) {
projects.put(library, library);
}
}
Set dirs = new HashSet<>();
for (Project project : projects.keySet()) {
assert !dirs.contains(project.getDir());
dirs.add(project.getDir());
}
}
return roots;
}
private void addProjects(
@NonNull File dir,
@NonNull Map fileToProject,
@NonNull File rootDir) {
if (canceled) {
return;
}
if (client.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 allLibraries = project.getAllLibraries();
Set allProjects = new HashSet<>(allLibraries.size() + 1);
allProjects.add(project);
allProjects.addAll(allLibraries);
currentProjects = allProjects.toArray(new Project[allProjects.size()]);
currentProject = project;
for (Detector check : applicableDetectors) {
check.beforeCheckProject(projectContext);
if (canceled) {
return;
}
}
assert currentProject == project;
runFileDetectors(project, main);
if (!Scope.checkSingleFile(scope)) {
List libraries = project.getAllLibraries();
for (Project library : libraries) {
Context libraryContext = new Context(this, library, project, projectDir);
fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext);
currentProject = library;
for (Detector check : applicableDetectors) {
check.beforeCheckLibraryProject(libraryContext);
if (canceled) {
return;
}
}
assert currentProject == library;
runFileDetectors(library, main);
if (canceled) {
return;
}
assert currentProject == library;
for (Detector check : applicableDetectors) {
check.afterCheckLibraryProject(libraryContext);
if (canceled) {
return;
}
}
}
}
currentProject = project;
for (Detector check : applicableDetectors) {
check.afterCheckProject(projectContext);
if (canceled) {
return;
}
}
if (canceled) {
client.report(
projectContext,
// Must provide an issue since API guarantees that the issue parameter
IssueRegistry.CANCELLED,
Severity.INFORMATIONAL,
Location.create(project.getDir()),
"Lint canceled by user", TextFormat.RAW);
}
currentProjects = 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 = client.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()))
&& scope.contains(Scope.MANIFEST)) {
List detectors = scopeDetectors.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 (scope.contains(Scope.ALL_RESOURCE_FILES)
|| scope.contains(Scope.RESOURCE_FILE)
|| scope.contains(Scope.RESOURCE_FOLDER)
|| scope.contains(Scope.BINARY_RESOURCE_FILE)) {
List dirChecks = scopeDetectors.get(Scope.RESOURCE_FOLDER);
List binaryChecks = scopeDetectors.get(Scope.BINARY_RESOURCE_FILE);
List checks = union(scopeDetectors.get(Scope.RESOURCE_FILE),
scopeDetectors.get(Scope.ALL_RESOURCE_FILES));
boolean haveXmlChecks = checks != null && !checks.isEmpty();
List xmlDetectors;
if (haveXmlChecks) {
xmlDetectors = new ArrayList<>(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 files = project.getSubset();
if (files != null) {
checkIndividualResources(project, main, xmlDetectors, dirChecks,
binaryChecks, files);
} else {
List resourceFolders = project.getResourceFolders();
if (!resourceFolders.isEmpty()) {
for (File res : resourceFolders) {
checkResFolder(project, main, res, xmlDetectors, dirChecks,
binaryChecks);
}
}
}
}
}
if (canceled) {
return;
}
}
if (scope.contains(Scope.JAVA_FILE) || scope.contains(Scope.ALL_JAVA_FILES)) {
List checks = union(scopeDetectors.get(Scope.JAVA_FILE),
scopeDetectors.get(Scope.ALL_JAVA_FILES));
if (checks != null && !checks.isEmpty()) {
List files = project.getSubset();
if (files != null) {
checkIndividualJavaFiles(project, main, checks, files);
} else {
List sourceFolders = project.getJavaSourceFolders();
List testFolders = scope.contains(Scope.TEST_SOURCES)
? project.getTestSourceFolders() : Collections.emptyList();
checkJava(project, main, sourceFolders, testFolders, checks);
}
}
}
if (canceled) {
return;
}
if (scope.contains(Scope.CLASS_FILE)
|| scope.contains(Scope.ALL_CLASS_FILES)
|| scope.contains(Scope.JAVA_LIBRARIES)) {
checkClasses(project, main);
}
if (canceled) {
return;
}
if (scope.contains(Scope.GRADLE_FILE)) {
checkBuildScripts(project, main);
}
if (canceled) {
return;
}
if (scope.contains(Scope.OTHER)) {
List checks = scopeDetectors.get(Scope.OTHER);
if (checks != null) {
OtherFileVisitor visitor = new OtherFileVisitor(checks);
visitor.scan(this, project, main);
}
}
if (canceled) {
return;
}
if (project == main && scope.contains(Scope.PROGUARD_FILE) &&
project.isAndroidProject()) {
checkProGuard(project, main);
}
if (project == main && scope.contains(Scope.PROPERTY_FILE)) {
checkProperties(project, main);
}
}
private void checkBuildScripts(Project project, Project main) {
List detectors = scopeDetectors.get(Scope.GRADLE_FILE);
if (detectors != null) {
List 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) {
detector.beforeCheckFile(context);
detector.visitBuildScript(context, Maps.newHashMap());
detector.afterCheckFile(context);
}
}
}
}
private void checkProGuard(Project project, Project main) {
List detectors = scopeDetectors.get(Scope.PROGUARD_FILE);
if (detectors != null) {
List files = project.getProguardFiles();
for (File file : files) {
Context context = new Context(this, project, main, file);
fireEvent(EventType.SCANNING_FILE, context);
for (Detector detector : detectors) {
detector.beforeCheckFile(context);
detector.run(context);
detector.afterCheckFile(context);
}
}
}
}
private void checkProperties(Project project, Project main) {
List detectors = scopeDetectors.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 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) {
detector.beforeCheckFile(context);
detector.run(context);
detector.afterCheckFile(context);
}
}
}
/** True if execution has been canceled */
boolean isCanceled() {
return canceled;
}
/**
* 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 client.getSuperClass(currentProject, 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 (currentProject != null) {
Boolean isSub = client.isSubclassOf(currentProject, 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 union(
@Nullable List list1,
@Nullable List 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 set = new HashSet<>(list1.size() + list2.size());
set.addAll(list1);
set.addAll(list2);
return new ArrayList<>(set);
}
}
/** Check the classes in this project (and if applicable, in any library projects */
private void checkClasses(Project project, Project main) {
List 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 libraries = project.getJavaLibraries(false);
List libraryEntries = ClassEntry.fromClassPath(client, libraries, true);
List classFolders = project.getJavaClassFolders();
List 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());
client.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(client, classFolders, true);
}
// Actually run the detectors. Libraries should be called before the
// main classes.
runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main);
if (canceled) {
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 files) {
List classFiles = Lists.newArrayListWithExpectedSize(files.size());
List 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 entries = ClassEntry.fromClassFiles(client, 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 mOuterClasses;
private void runClassDetectors(Scope scope, List entries,
Project project, Project main) {
if (this.scope.contains(scope)) {
List classDetectors = scopeDetectors.get(scope);
if (classDetectors != null && !classDetectors.isEmpty() && !entries.isEmpty()) {
AsmVisitor visitor = new AsmVisitor(client, classDetectors);
CharSequence sourceContents = null;
String sourceName = "";
mOuterClasses = new ArrayDeque<>();
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) {
client.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) {
client.log(e, null);
}
if (canceled) {
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 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 = client.readBytes(classFile);
ClassReader reader = new ClassReader(bytes);
ClassNode classNode = new ClassNode();
reader.accept(classNode, flags);
return classNode;
} catch (Throwable t) {
client.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 : client.getJavaClassFolders(project)) {
File path = new File(root, relativePath);
if (path.exists()) {
return path;
}
}
// Search in the libraries
for (File root : client.getJavaLibraries(project, true)) {
// 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 sourceFolders,
@NonNull List testSourceFolders,
@NonNull List checks) {
JavaParser javaParser = client.getJavaParser(project);
if (javaParser == null) {
client.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 sources = new ArrayList<>(100);
for (File folder : sourceFolders) {
gatherJavaFiles(folder, sources);
}
List contexts = Lists.newArrayListWithExpectedSize(2 * sources.size());
for (File file : sources) {
JavaContext context = new JavaContext(this, project, main, file, javaParser);
contexts.add(context);
}
// Test sources
sources.clear();
for (File folder : testSourceFolders) {
gatherJavaFiles(folder, sources);
}
for (File file : sources) {
JavaContext context = new JavaContext(this, project, main, file, javaParser);
context.setTestSource(true);
contexts.add(context);
}
// Visit all contexts
if (!contexts.isEmpty()) {
visitJavaFiles(checks, javaParser, contexts);
}
}
private void visitJavaFiles(@NonNull List checks, JavaParser javaParser,
List contexts) {
// Temporary: we still have some builtin checks that aren't migrated to
// PSI. Until that's complete, remove them from the list here
//List scanners = checks;
List scanners = Lists.newArrayListWithCapacity(checks.size());
for (Detector detector : checks) {
if (detector instanceof Detector.JavaPsiScanner) {
scanners.add(detector);
}
}
JavaPsiVisitor visitor = new JavaPsiVisitor(javaParser, scanners);
if (runCompatChecks) {
visitor.setDisposeUnitsAfterUse(false);
}
parserErrors = !visitor.prepare(contexts);
for (JavaContext context : contexts) {
fireEvent(EventType.SCANNING_FILE, context);
visitor.visitFile(context);
if (canceled) {
return;
}
}
// Only if the user is using some custom lint rules that haven't been updated
// yet
//noinspection ConstantConditions
if (runCompatChecks) {
try {
// Call EcjParser#disposePsi (if running from Gradle) to clear up PSI
// caches that are full from the above JavaPsiVisitor call. We do this
// instead of calling visitor.dispose because we want to *keep* the
// ECJ parse around for use by the Lombok bridge.
javaParser.getClass().getMethod("disposePsi").invoke(javaParser);
} catch (Throwable ignore) {
}
// Filter the checks to only those that implement JavaScanner
List filtered = Lists.newArrayListWithCapacity(checks.size());
for (Detector detector : checks) {
if (detector instanceof Detector.JavaScanner) {
assert !(detector instanceof Detector.JavaPsiScanner); // Shouldn't be both
filtered.add(detector);
}
}
if (!filtered.isEmpty()) {
List detectorNames = Lists.newArrayListWithCapacity(filtered.size());
for (Detector detector : filtered) {
detectorNames.add(detector.getClass().getName());
}
Collections.sort(detectorNames);
/* Let's not complain quite yet
String message = String.format("Lint found one or more custom checks using its "
+ "older Java API; these checks are still run in compatibility mode, "
+ "but this causes duplicated parsing, and in the next version lint "
+ "will no longer include this legacy mode. Make sure the following "
+ "lint detectors are upgraded to the new API: %1$s",
Joiner.on(", ").join(detectorNames));
JavaContext first = contexts.get(0);
Project project = first.getProject();
Location location = Location.create(project.getDir());
client.report(first,
IssueRegistry.LINT_ERROR,
project.getConfiguration(this).getSeverity(IssueRegistry.LINT_ERROR),
location, message, TextFormat.RAW);
*/
JavaVisitor oldVisitor = new JavaVisitor(javaParser, filtered);
// NOTE: We do NOT call oldVisitor.prepare and dispose here since this
// visitor is wrapping the same java parser as the one we used for PSI,
// so calling prepare again would wipe out the results we're trying to reuse.
for (JavaContext context : contexts) {
fireEvent(EventType.SCANNING_FILE, context);
oldVisitor.visitFile(context);
if (canceled) {
return;
}
}
}
}
visitor.dispose();
}
private void checkIndividualJavaFiles(
@NonNull Project project,
@Nullable Project main,
@NonNull List checks,
@NonNull List files) {
JavaParser javaParser = client.getJavaParser(project);
if (javaParser == null) {
client.log(null, "No java parser provided to lint: not running Java checks");
return;
}
List 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;
}
visitJavaFiles(checks, javaParser, contexts);
}
private static void gatherJavaFiles(@NonNull File dir, @NonNull List result) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(DOT_JAVA)) {
result.add(file);
} else if (file.isDirectory()) {
gatherJavaFiles(file, result);
}
}
}
}
private ResourceFolderType currentFolderType;
private List currentXmlDetectors;
private List currentBinaryDetectors;
private ResourceVisitor currentVisitor;
@Nullable
private ResourceVisitor getVisitor(
@NonNull ResourceFolderType type,
@NonNull List checks,
@Nullable List binaryChecks) {
if (type != currentFolderType) {
currentFolderType = type;
// Determine which XML resource detectors apply to the given folder type
List applicableXmlChecks =
new ArrayList<>(checks.size());
for (ResourceXmlDetector check : checks) {
if (check.appliesTo(type)) {
applicableXmlChecks.add(check);
}
}
List applicableBinaryChecks = null;
if (binaryChecks != null) {
applicableBinaryChecks = new ArrayList<>(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 (currentXmlDetectors != null && currentXmlDetectors.equals(applicableXmlChecks)
&& Objects.equal(currentBinaryDetectors, applicableBinaryChecks)) {
return currentVisitor;
}
currentXmlDetectors = applicableXmlChecks;
currentBinaryDetectors = applicableBinaryChecks;
if (applicableXmlChecks.isEmpty()
&& (applicableBinaryChecks == null || applicableBinaryChecks.isEmpty())) {
currentVisitor = null;
return null;
}
XmlParser parser = client.getXmlParser();
if (parser != null) {
currentVisitor = new ResourceVisitor(parser, applicableXmlChecks,
applicableBinaryChecks);
} else {
currentVisitor = null;
}
}
return currentVisitor;
}
private void checkResFolder(
@NonNull Project project,
@Nullable Project main,
@NonNull File res,
@NonNull List xmlChecks,
@Nullable List dirChecks,
@Nullable List 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 (canceled) {
return;
}
}
}
private void checkResourceFolder(
@NonNull Project project,
@Nullable Project main,
@NonNull File dir,
@NonNull ResourceFolderType type,
@NonNull List xmlChecks,
@Nullable List dirChecks,
@Nullable List 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) ||
type == ResourceFolderType.RAW)) {
ResourceContext context = new ResourceContext(this, project, main, file, type);
fireEvent(EventType.SCANNING_FILE, context);
visitor.visitBinaryResource(context);
}
if (canceled) {
return;
}
}
}
}
/** Checks individual resources */
private void checkIndividualResources(
@NonNull Project project,
@Nullable Project main,
@NonNull List xmlDetectors,
@Nullable List dirChecks,
@Nullable List binaryChecks,
@NonNull List 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 {
client.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 (canceled) {
return;
}
}
}
}
}
}
/**
* Adds a listener to be notified of lint progress
*
* @param listener the listener to be added
*/
public void addLintListener(@NonNull LintListener listener) {
if (listeners == null) {
listeners = new ArrayList<>(1);
}
listeners.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) {
listeners.remove(listener);
if (listeners.isEmpty()) {
listeners = null;
}
}
/** Notifies listeners, if any, that the given event has occurred */
private void fireEvent(@NonNull LintListener.EventType type, @Nullable Context context) {
if (listeners != null) {
for (LintListener listener : listeners) {
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) {
super(getClientName());
mDelegate = delegate;
}
@Override
public void report(
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@NonNull Location location,
@NonNull String message,
@NonNull TextFormat format) {
//noinspection ConstantConditions
if (location == null) {
// Misbehaving third-party lint detectors
assert false : issue;
return;
}
assert currentProject != null;
if (!currentProject.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;
}
if (baseline != null) {
boolean filtered = baseline.findAndMark(issue, location, message, severity,
context.getProject());
if (filtered) {
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);
}
@NonNull
@Override
public List getTestLibraries(@NonNull Project project) {
return mDelegate.getTestLibraries(project);
}
@Nullable
@Override
public String getClientRevision() {
return mDelegate.getClientRevision();
}
@Override
public void runReadAction(@NonNull Runnable runnable) {
mDelegate.runReadAction(runnable);
}
@Override
@NonNull
public CharSequence 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 getJavaSourceFolders(@NonNull Project project) {
return mDelegate.getJavaSourceFolders(project);
}
@Override
@NonNull
public List getJavaClassFolders(@NonNull Project project) {
return mDelegate.getJavaClassFolders(project);
}
@NonNull
@Override
public List getJavaLibraries(@NonNull Project project, boolean includeProvided) {
return mDelegate.getJavaLibraries(project, includeProvided);
}
@NonNull
@Override
public List getTestSourceFolders(@NonNull Project project) {
return mDelegate.getTestSourceFolders(project);
}
@Override
public Collection getKnownProjects() {
return mDelegate.getKnownProjects();
}
@Nullable
@Override
public BuildToolInfo getBuildTools(@NonNull Project project) {
return mDelegate.getBuildTools(project);
}
@NonNull
@Override
public Map createSuperClassMap(@NonNull Project project) {
return mDelegate.createSuperClassMap(project);
}
@NonNull
@Override
public ResourceVisibilityLookup.Provider getResourceVisibilityProvider() {
return mDelegate.getResourceVisibilityProvider();
}
@Override
@NonNull
public List 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 AndroidSdkHandler 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 findGlobalRuleJars() {
return mDelegate.findGlobalRuleJars();
}
@NonNull
@Override
public List 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);
}
@NonNull
@Override
public List getAssetFolders(@NonNull Project project) {
return mDelegate.getAssetFolders(project);
}
@Override
public ClassLoader createUrlClassLoader(@NonNull URL[] urls, @NonNull ClassLoader parent) {
return mDelegate.createUrlClassLoader(urls, parent);
}
@Override
public boolean checkForSuppressComments() {
return mDelegate.checkForSuppressComments();
}
@Override
public boolean supportsProjectResources() {
return mDelegate.supportsProjectResources();
}
@SuppressWarnings("deprecation") // forwarding required API
@Nullable
@Override
public AbstractResourceRepository getProjectResources(Project project,
boolean includeDependencies) {
return mDelegate.getProjectResources(project, includeDependencies);
}
@Nullable
@Override
public AbstractResourceRepository getResourceRepository(Project project,
boolean includeModuleDependencies, boolean includeLibraries) {
return mDelegate.getResourceRepository(project, includeModuleDependencies,
includeLibraries);
}
@NonNull
@Override
public ProgressIndicator getRepositoryLogger() {
return mDelegate.getRepositoryLogger();
}
@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 knows 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) {
if (repeatingDetectors == null) {
repeatingDetectors = new ArrayList<>();
}
repeatingDetectors.add(detector);
if (scope != null) {
if (repeatScope == null) {
repeatScope = scope;
} else {
repeatScope = EnumSet.copyOf(repeatScope);
repeatScope.addAll(scope);
}
} else {
repeatScope = 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 annotations = method.invisibleAnnotations;
return isSuppressed(issue, annotations);
}
// Initializations of fields end up placed in generated methods (
// for members and 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 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 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 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")) {
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 true if the given issue is suppressed by the given suppress string; this
* is typically the same as the issue id, but is allowed to not match case sensitively,
* and is allowed to be a comma separated list, and can be the string "all"
*
* @param issue the issue id to match
* @param string the suppress string -- typically the id, or "all", or a comma separated list
* of ids
* @return true if the issue is suppressed by the given string
*/
private static boolean isSuppressed(@NonNull Issue issue, @NonNull String string) {
if (string.isEmpty()) {
return false;
}
if (string.indexOf(',') == -1) {
if (matches(issue, string)) {
return true;
}
} else {
for (String id : Splitter.on(',').trimResults().split(string)) {
if (matches(issue, id)) {
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 = client.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;
}
} else if (type == AnnotationMethodDeclaration.class) {
// Look for annotations on the method
AnnotationMethodDeclaration declaration = (AnnotationMethodDeclaration) scope;
if (isSuppressed(issue, declaration.astModifiers())) {
return true;
}
}
if (checkComments && context.isSuppressedWithComment(scope, issue)) {
return true;
}
scope = scope.getParent();
}
return false;
}
public boolean isSuppressed(@Nullable JavaContext context, @NonNull Issue issue,
@Nullable PsiElement scope) {
boolean checkComments = client.checkForSuppressComments() &&
context != null && context.containsCommentSuppress();
while (scope != null) {
if (scope instanceof PsiModifierListOwner) {
PsiModifierListOwner owner = (PsiModifierListOwner) scope;
if (isSuppressed(issue, owner.getModifierList())) {
return true;
}
}
if (checkComments && context.isSuppressedWithComment(scope, issue)) {
return true;
}
scope = scope.getParent();
if (scope instanceof PsiFile) {
return false;
}
}
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 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")) {
StrictListAccessor 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 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;
}
public static final String SUPPRESS_WARNINGS_FQCN = "java.lang.SuppressWarnings";
/**
* 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 modifierList the modifier to check
* @return true if the issue or all issues should be suppressed for this
* modifier
*/
public static boolean isSuppressed(@NonNull Issue issue,
@Nullable PsiModifierList modifierList) {
if (modifierList == null) {
return false;
}
for (PsiAnnotation annotation : modifierList.getAnnotations()) {
String fqcn = annotation.getQualifiedName();
if (fqcn != null && (fqcn.equals(FQCN_SUPPRESS_LINT)
|| fqcn.equals(SUPPRESS_WARNINGS_FQCN)
|| fqcn.equals(SUPPRESS_LINT))) { // when missing imports
PsiAnnotationParameterList parameterList = annotation.getParameterList();
for (PsiNameValuePair pair : parameterList.getAttributes()) {
if (isSuppressed(issue, pair.getValue())) {
return true;
}
}
}
}
return false;
}
/**
* Returns true if the annotation member value, assumed to be specified on a a SuppressWarnings
* or SuppressLint annotation, specifies the given id (or "all").
*
* @param issue the issue to be checked
* @param value the member value to check
* @return true if the issue or all issues should be suppressed for this modifier
*/
public static boolean isSuppressed(@NonNull Issue issue,
@Nullable PsiAnnotationMemberValue value) {
if (value instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)value;
Object literalValue = literal.getValue();
if (literalValue instanceof String) {
if (isSuppressed(issue, (String) literalValue)) {
return true;
}
}
} else if (value instanceof PsiArrayInitializerMemberValue) {
PsiArrayInitializerMemberValue mv = (PsiArrayInitializerMemberValue)value;
for (PsiAnnotationMemberValue mmv : mv.getInitializers()) {
if (isSuppressed(issue, mmv)) {
return true;
}
}
} else if (value instanceof PsiArrayInitializerExpression) {
PsiArrayInitializerExpression expression = (PsiArrayInitializerExpression) value;
PsiExpression[] initializers = expression.getInitializers();
for (PsiExpression e : initializers) {
if (isSuppressed(issue, e)) {
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 = client.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 (isSuppressed(issue, ignore)) {
return true;
}
} else if (checkComments && context.isSuppressedWithComment(node, issue)) {
return true;
}
}
node = node.getParentNode();
}
return false;
}
private File cachedFolder = null;
private int cachedFolderVersion = -1;
/** Pattern for version qualifiers */
private static final Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$");
/**
* 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(cachedFolder)) {
return cachedFolderVersion;
}
cachedFolder = parent;
cachedFolderVersion = -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;
cachedFolderVersion = Integer.parseInt(group);
break;
}
}
return cachedFolderVersion;
}
}