| /* |
| * 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(); |
| } |
| |
| /** |
| * 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 |
| * @param data any associated data, or null |
| */ |
| public void report( |
| @NonNull Issue issue, |
| @Nullable Location location, |
| @NonNull String message, |
| @Nullable Object data) { |
| 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(); |
| } |
| } |
| |
| // 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, data); |
| } |
| |
| /** |
| * 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 == -1) { |
| 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 prevLineStart = contents.lastIndexOf('\n', lineStart - 2) + 1; |
| if (prevLineStart == 0) { |
| 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; |
| } |
| } |