blob: 6eca8163d08e25c8ad7c7bdd696f4cae2049dc14 [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.detector.api;
import static com.android.SdkConstants.DOT_GRADLE;
import static com.android.SdkConstants.DOT_JAVA;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.SUPPRESS_ALL;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.client.api.SdkInfo;
import com.google.common.annotations.Beta;
import java.io.File;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
/**
* Context passed to the detectors during an analysis run. It provides
* information about the file being analyzed, it allows shared properties (so
* the detectors can share results), etc.
* <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 Context {
/**
* The file being checked. Note that this may not always be to a concrete
* file. For example, in the {@link Detector#beforeCheckProject(Context)}
* method, the context file is the directory of the project.
*/
public final File file;
/** The driver running through the checks */
protected final LintDriver mDriver;
/** The project containing the file being checked */
@NonNull
private final Project mProject;
/**
* The "main" project. For normal projects, this is the same as {@link #mProject},
* but for library projects, it's the root project that includes (possibly indirectly)
* the various library projects and their library projects.
* <p>
* Note that this is a property on the {@link Context}, not the
* {@link Project}, since a library project can be included from multiple
* different top level projects, so there isn't <b>one</b> main project,
* just one per main project being analyzed with its library projects.
*/
private final Project mMainProject;
/** The current configuration controlling which checks are enabled etc */
private final Configuration mConfiguration;
/** The contents of the file */
private String mContents;
/** Map of properties to share results between detectors */
private Map<String, Object> mProperties;
/** Whether this file contains any suppress markers (null means not yet determined) */
private Boolean mContainsCommentSuppress;
/**
* Construct a new {@link Context}
*
* @param driver the driver running through the checks
* @param project the project containing the file being checked
* @param main the main project if this project is a library project, or
* null if this is not a library project. The main project is
* the root project of all library projects, not necessarily the
* directly including project.
* @param file the file being checked
*/
public Context(
@NonNull LintDriver driver,
@NonNull Project project,
@Nullable Project main,
@NonNull File file) {
this.file = file;
mDriver = driver;
mProject = project;
mMainProject = main;
mConfiguration = project.getConfiguration(driver);
}
/**
* Returns the scope for the lint job
*
* @return the scope, never null
*/
@NonNull
public EnumSet<Scope> getScope() {
return mDriver.getScope();
}
/**
* Returns the configuration for this project.
*
* @return the configuration, never null
*/
@NonNull
public Configuration getConfiguration() {
return mConfiguration;
}
/**
* Returns the project containing the file being checked
*
* @return the project, never null
*/
@NonNull
public Project getProject() {
return mProject;
}
/**
* Returns the main project if this project is a library project, or self
* if this is not a library project. The main project is the root project
* of all library projects, not necessarily the directly including project.
*
* @return the main project, never null
*/
@NonNull
public Project getMainProject() {
return mMainProject != null ? mMainProject : mProject;
}
/**
* Returns the lint client requesting the lint check
*
* @return the client, never null
*/
@NonNull
public LintClient getClient() {
return mDriver.getClient();
}
/**
* Returns the driver running through the lint checks
*
* @return the driver
*/
@NonNull
public LintDriver getDriver() {
return mDriver;
}
/**
* Returns the contents of the file. This may not be the contents of the
* file on disk, since it delegates to the {@link LintClient}, which in turn
* may decide to return the current edited contents of the file open in an
* editor.
*
* @return the contents of the given file, or null if an error occurs.
*/
@Nullable
public String getContents() {
if (mContents == null) {
mContents = mDriver.getClient().readFile(file);
}
return mContents;
}
/**
* Returns the value of the given named property, or null.
*
* @param name the name of the property
* @return the corresponding value, or null
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
@Nullable
public Object getProperty(String name) {
if (mProperties == null) {
return null;
}
return mProperties.get(name);
}
/**
* Sets the value of the given named property.
*
* @param name the name of the property
* @param value the corresponding value
*/
@SuppressWarnings("UnusedDeclaration") // Used in ADT
public void setProperty(@NonNull String name, @Nullable Object value) {
if (value == null) {
if (mProperties != null) {
mProperties.remove(name);
}
} else {
if (mProperties == null) {
mProperties = new HashMap<String, Object>();
}
mProperties.put(name, value);
}
}
/**
* Gets the SDK info for the current project.
*
* @return the SDK info for the current project, never null
*/
@NonNull
public SdkInfo getSdkInfo() {
return mProject.getSdkInfo();
}
// ---- Convenience wrappers ---- (makes the detector code a bit leaner)
/**
* Returns false if the given issue has been disabled. Convenience wrapper
* around {@link Configuration#getSeverity(Issue)}.
*
* @param issue the issue to check
* @return false if the issue has been disabled
*/
public boolean isEnabled(@NonNull Issue issue) {
return mConfiguration.isEnabled(issue);
}
/**
* Reports an issue. Convenience wrapper around {@link LintClient#report}
*
* @param issue the issue to report
* @param location the location of the issue, or null if not known
* @param message the message for this warning
*/
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message) {
Configuration configuration = mConfiguration;
// If this error was computed for a context where the context corresponds to
// a project instead of a file, the actual error may be in a different project (e.g.
// a library project), so adjust the configuration as necessary.
if (location != null && location.getFile() != null) {
Project project = mDriver.findProjectFor(location.getFile());
if (project != null) {
configuration = project.getConfiguration(mDriver);
}
}
// If an error occurs in a library project, but you've disabled that check in the
// main project, disable it in the library project too. (In some cases you don't
// control the lint.xml of a library project, and besides, if you're not interested in
// a check for your main project you probably don't care about it in the library either.)
if (configuration != mConfiguration
&& mConfiguration.getSeverity(issue) == Severity.IGNORE) {
return;
}
Severity severity = configuration.getSeverity(issue);
if (severity == Severity.IGNORE) {
return;
}
mDriver.getClient().report(this, issue, severity, location, message, TextFormat.RAW);
}
/**
* Report an error.
* Like {@link #report(Issue, Location, String)} but with
* a now-unused data parameter at the end
*
* @deprecated Use {@link #report(Issue, Location, String)} instead;
* this method is here for custom rule compatibility
*/
@SuppressWarnings("UnusedDeclaration") // Potentially used by external existing custom rules
@Deprecated
public void report(
@NonNull Issue issue,
@Nullable Location location,
@NonNull String message,
@SuppressWarnings("UnusedParameters") @Nullable Object data) {
report(issue, location, message);
}
/**
* Send an exception to the log. Convenience wrapper around {@link LintClient#log}.
*
* @param exception the exception, possibly null
* @param format the error message using {@link String#format} syntax, possibly null
* @param args any arguments for the format string
*/
public void log(
@Nullable Throwable exception,
@Nullable String format,
@Nullable Object... args) {
mDriver.getClient().log(exception, format, args);
}
/**
* 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 mDriver.getPhase();
}
/**
* 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) {
mDriver.requestRepeat(detector, scope);
}
/** Returns the comment marker used in Studio to suppress statements for language, if any */
@Nullable
protected String getSuppressCommentPrefix() {
// Java and XML files are handled in sub classes (XmlContext, JavaContext)
String path = file.getPath();
if (path.endsWith(DOT_JAVA) || path.endsWith(DOT_GRADLE)) {
return JavaContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(DOT_XML)) {
return XmlContext.SUPPRESS_COMMENT_PREFIX;
} else if (path.endsWith(".cfg") || path.endsWith(".pro")) {
return "#suppress ";
}
return null;
}
/** Returns whether this file contains any suppress comment markers */
public boolean containsCommentSuppress() {
if (mContainsCommentSuppress == null) {
mContainsCommentSuppress = false;
String prefix = getSuppressCommentPrefix();
if (prefix != null) {
String contents = getContents();
if (contents != null) {
mContainsCommentSuppress = contents.contains(prefix);
}
}
}
return mContainsCommentSuppress;
}
/**
* Returns true if the given issue is suppressed at the given character offset
* in the file's contents
*/
public boolean isSuppressedWithComment(int startOffset, @NonNull Issue issue) {
String prefix = getSuppressCommentPrefix();
if (prefix == null) {
return false;
}
if (startOffset <= 0) {
return false;
}
// Check whether there is a comment marker
String contents = getContents();
assert contents != null; // otherwise we wouldn't be here
if (startOffset >= contents.length()) {
return false;
}
// Scan backwards to the previous line and see if it contains the marker
int lineStart = contents.lastIndexOf('\n', startOffset) + 1;
if (lineStart <= 1) {
return false;
}
int index = findPrefixOnPreviousLine(contents, lineStart, prefix);
if (index != -1 &&index+prefix.length() < lineStart) {
String line = contents.substring(index + prefix.length(), lineStart);
return line.contains(issue.getId())
|| line.contains(SUPPRESS_ALL) && line.trim().startsWith(SUPPRESS_ALL);
}
return false;
}
private static int findPrefixOnPreviousLine(String contents, int lineStart, String prefix) {
// Search backwards on the previous line until you find the prefix start (also look
// back on previous lines if the previous line(s) contain just whitespace
char first = prefix.charAt(0);
int offset = lineStart - 2; // 0: first char on this line, -1: \n on previous line, -2 last
boolean seenNonWhitespace = false;
for (; offset >= 0; offset--) {
char c = contents.charAt(offset);
if (seenNonWhitespace && c == '\n') {
return -1;
}
if (!seenNonWhitespace && !Character.isWhitespace(c)) {
seenNonWhitespace = true;
}
if (c == first && contents.regionMatches(false, offset, prefix, 0,
prefix.length())) {
return offset;
}
}
return -1;
}
}