blob: 284e41b096866300080e168f1aeefbb7684d3f12 [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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.blame.SourcePosition;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.google.common.annotations.Beta;
import java.io.File;
/**
* Location information for a warning
* <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 Location {
private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$
private final File mFile;
private final Position mStart;
private final Position mEnd;
private String mMessage;
private Location mSecondary;
private Object mClientData;
/**
* (Private constructor, use one of the factory methods
* {@link Location#create(File)},
* {@link Location#create(File, Position, Position)}, or
* {@link Location#create(File, String, int, int)}.
* <p>
* Constructs a new location range for the given file, from start to end. If
* the length of the range is not known, end may be null.
*
* @param file the associated file (but see the documentation for
* {@link #getFile()} for more information on what the file
* represents)
* @param start the starting position, or null
* @param end the ending position, or null
*/
protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) {
super();
mFile = file;
mStart = start;
mEnd = end;
}
/**
* Returns the file containing the warning. Note that the file *itself* may
* not yet contain the error. When editing a file in the IDE for example,
* the tool could generate warnings in the background even before the
* document is saved. However, the file is used as a identifying token for
* the document being edited, and the IDE integration can map this back to
* error locations in the editor source code.
*
* @return the file handle for the location
*/
@NonNull
public File getFile() {
return mFile;
}
/**
* The start position of the range
*
* @return the start position of the range, or null
*/
@Nullable
public Position getStart() {
return mStart;
}
/**
* The end position of the range
*
* @return the start position of the range, may be null for an empty range
*/
@Nullable
public Position getEnd() {
return mEnd;
}
/**
* Returns a secondary location associated with this location (if
* applicable), or null.
*
* @return a secondary location or null
*/
@Nullable
public Location getSecondary() {
return mSecondary;
}
/**
* Sets a secondary location for this location.
*
* @param secondary a secondary location associated with this location
*/
public void setSecondary(@Nullable Location secondary) {
mSecondary = secondary;
}
/**
* Sets a custom message for this location. This is typically used for
* secondary locations, to describe the significance of this alternate
* location. For example, for a duplicate id warning, the primary location
* might say "This is a duplicate id", pointing to the second occurrence of
* id declaration, and then the secondary location could point to the
* original declaration with the custom message "Originally defined here".
*
* @param message the message to apply to this location
*/
public void setMessage(@NonNull String message) {
mMessage = message;
}
/**
* Returns the custom message for this location, if any. This is typically
* used for secondary locations, to describe the significance of this
* alternate location. For example, for a duplicate id warning, the primary
* location might say "This is a duplicate id", pointing to the second
* occurrence of id declaration, and then the secondary location could point
* to the original declaration with the custom message
* "Originally defined here".
*
* @return the custom message for this location, or null
*/
@Nullable
public String getMessage() {
return mMessage;
}
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
public Object getClientData() {
return mClientData;
}
@Override
public String toString() {
return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message="
+ mMessage + ']';
}
/**
* Creates a new location for the given file
*
* @param file the file to create a location for
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file) {
return new Location(file, null /*start*/, null /*end*/);
}
/**
* Creates a new location for the given file and SourcePosition.
*
* @param file the file containing the positions
* @param position the source position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull SourcePosition position) {
if (position.equals(SourcePosition.UNKNOWN)) {
return new Location(file, null /*start*/, null /*end*/);
}
return new Location(file,
new DefaultPosition(
position.getStartLine(),
position.getStartColumn(),
position.getStartOffset()),
new DefaultPosition(
position.getEndLine(),
position.getEndColumn(),
position.getEndOffset()));
}
/**
* Creates a new location for the given file and starting and ending
* positions.
*
* @param file the file containing the positions
* @param start the starting position
* @param end the ending position
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@NonNull Position start,
@Nullable Position end) {
return new Location(file, start, end);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given offset range.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param startOffset the starting offset
* @param endOffset the ending offset
* @return a new location
*/
@NonNull
public static Location create(
@NonNull File file,
@Nullable String contents,
int startOffset,
int endOffset) {
if (startOffset < 0 || endOffset < startOffset) {
throw new IllegalArgumentException("Invalid offsets");
}
if (contents == null) {
return new Location(file,
new DefaultPosition(-1, -1, startOffset),
new DefaultPosition(-1, -1, endOffset));
}
int size = contents.length();
endOffset = Math.min(endOffset, size);
startOffset = Math.min(startOffset, endOffset);
Position start = null;
int line = 0;
int lineOffset = 0;
char prev = 0;
for (int offset = 0; offset <= size; offset++) {
if (offset == startOffset) {
start = new DefaultPosition(line, offset - lineOffset, offset);
}
if (offset == endOffset) {
Position end = new DefaultPosition(line, offset - lineOffset, offset);
return new Location(file, start, end);
}
char c = contents.charAt(offset);
if (c == '\n') {
lineOffset = offset + 1;
if (prev != '\r') {
line++;
}
} else if (c == '\r') {
line++;
lineOffset = offset + 1;
}
prev = c;
}
return create(file);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line) {
return create(file, contents, line, null, null, null);
}
/**
* Creates a new location for the given file, with the given contents, for
* the given line number.
*
* @param file the file containing the location
* @param contents the current contents of the file
* @param line the line number (0-based) for the position
* @param patternStart an optional pattern to search for from the line
* match; if found, adjust the column and offsets to begin at the
* pattern start
* @param patternEnd an optional pattern to search for behind the start
* pattern; if found, adjust the end offset to match the end of
* the pattern
* @param hints optional additional information regarding the pattern search
* @return a new location
*/
@NonNull
public static Location create(@NonNull File file, @NonNull String contents, int line,
@Nullable String patternStart, @Nullable String patternEnd,
@Nullable SearchHints hints) {
int currentLine = 0;
int offset = 0;
while (currentLine < line) {
offset = contents.indexOf('\n', offset);
if (offset == -1) {
return create(file);
}
currentLine++;
offset++;
}
if (line == currentLine) {
if (patternStart != null) {
SearchDirection direction = SearchDirection.NEAREST;
if (hints != null) {
direction = hints.mDirection;
}
int index;
if (direction == SearchDirection.BACKWARD) {
index = findPreviousMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.EOL_BACKWARD) {
int lineEnd = contents.indexOf('\n', offset);
if (lineEnd == -1) {
lineEnd = contents.length();
}
index = findPreviousMatch(contents, lineEnd, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else if (direction == SearchDirection.FORWARD) {
index = findNextMatch(contents, offset, patternStart, hints);
line = adjustLine(contents, line, offset, index);
} else {
assert direction == SearchDirection.NEAREST;
int before = findPreviousMatch(contents, offset, patternStart, hints);
int after = findNextMatch(contents, offset, patternStart, hints);
if (before == -1) {
index = after;
line = adjustLine(contents, line, offset, index);
} else if (after == -1) {
index = before;
line = adjustLine(contents, line, offset, index);
} else if (offset - before < after - offset) {
index = before;
line = adjustLine(contents, line, offset, index);
} else {
index = after;
line = adjustLine(contents, line, offset, index);
}
}
if (index != -1) {
int lineStart = contents.lastIndexOf('\n', index);
if (lineStart == -1) {
lineStart = 0;
} else {
lineStart++; // was pointing to the previous line's CR, not line start
}
int column = index - lineStart;
if (patternEnd != null) {
int end = contents.indexOf(patternEnd, offset + patternStart.length());
if (end != -1) {
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, -1, end + patternEnd.length()));
}
} else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) {
if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) {
patternStart = SUPER_KEYWORD;
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column + patternStart.length(),
index + patternStart.length()));
}
return new Location(file, new DefaultPosition(line, column, index),
new DefaultPosition(line, column, index + patternStart.length()));
}
}
Position position = new DefaultPosition(line, -1, offset);
return new Location(file, position, position);
}
return create(file);
}
private static int findPreviousMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
while (true) {
int index = contents.lastIndexOf(pattern, offset);
if (index == -1) {
return -1;
} else {
if (isMatch(contents, index, pattern, hints)) {
return index;
} else {
offset = index - pattern.length();
}
}
}
}
private static int findNextMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
int constructorIndex = -1;
if (hints != null && hints.isConstructor()) {
// Special condition: See if the call is referenced as "super" instead.
assert hints.isWholeWord();
int index = contents.indexOf(SUPER_KEYWORD, offset);
if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) {
constructorIndex = index;
}
}
while (true) {
int index = contents.indexOf(pattern, offset);
if (index == -1) {
return constructorIndex;
} else {
if (isMatch(contents, index, pattern, hints)) {
if (constructorIndex != -1) {
return Math.min(constructorIndex, index);
}
return index;
} else {
offset = index + pattern.length();
}
}
}
}
private static boolean isMatch(@NonNull String contents, int offset, String pattern,
@Nullable SearchHints hints) {
if (!contents.startsWith(pattern, offset)) {
return false;
}
if (hints != null) {
char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0;
int lastIndex = offset + pattern.length() - 1;
char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0;
if (hints.isWholeWord() && (Character.isLetter(prevChar)
|| Character.isLetter(nextChar))) {
return false;
}
if (hints.isJavaSymbol()) {
if (Character.isJavaIdentifierPart(prevChar)
|| Character.isJavaIdentifierPart(nextChar)) {
return false;
}
if (prevChar == '"') {
return false;
}
// TODO: Additional validation to see if we're in a comment, string, etc.
// This will require lexing from the beginning of the buffer.
}
if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) {
// Only looking for super(), not super.x, so assert that the next
// non-space character is (
int index = lastIndex + 1;
while (index < contents.length() - 1) {
char c = contents.charAt(index);
if (c == '(') {
break;
} else if (!Character.isWhitespace(c)) {
return false;
}
index++;
}
}
}
return true;
}
private static int adjustLine(String doc, int line, int offset, int newOffset) {
if (newOffset == -1) {
return line;
}
if (newOffset < offset) {
return line - countLines(doc, newOffset, offset);
} else {
return line + countLines(doc, offset, newOffset);
}
}
private static int countLines(String doc, int start, int end) {
int lines = 0;
for (int offset = start; offset < end; offset++) {
char c = doc.charAt(offset);
if (c == '\n') {
lines++;
}
}
return lines;
}
/**
* Reverses the secondary location list initiated by the given location
*
* @param location the first location in the list
* @return the first location in the reversed list
*/
public static Location reverse(@NonNull Location location) {
Location next = location.getSecondary();
location.setSecondary(null);
while (next != null) {
Location nextNext = next.getSecondary();
next.setSecondary(location);
location = next;
next = nextNext;
}
return location;
}
/**
* A {@link Handle} is a reference to a location. The point of a location
* handle is to be able to create them cheaply, and then resolve them into
* actual locations later (if needed). This makes it possible to for example
* delay looking up line numbers, for locations that are offset based.
*/
public interface Handle {
/**
* Compute a full location for the given handle
*
* @return create a location for this handle
*/
@NonNull
Location resolve();
/**
* Sets the client data associated with this location. This is an optional
* field which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @param clientData the data to store with this location
*/
void setClientData(@Nullable Object clientData);
/**
* Returns the client data associated with this location - an optional field
* which can be used by the creator of the {@link Location} to store
* temporary state associated with the location.
*
* @return the data associated with this location
*/
@Nullable
Object getClientData();
}
/** A default {@link Handle} implementation for simple file offsets */
public static class DefaultLocationHandle implements Handle {
private final File mFile;
private final String mContents;
private final int mStartOffset;
private final int mEndOffset;
private Object mClientData;
/**
* Constructs a new {@link DefaultLocationHandle}
*
* @param context the context pointing to the file and its contents
* @param startOffset the start offset within the file
* @param endOffset the end offset within the file
*/
public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) {
mFile = context.file;
mContents = context.getContents();
mStartOffset = startOffset;
mEndOffset = endOffset;
}
@Override
@NonNull
public Location resolve() {
return create(mFile, mContents, mStartOffset, mEndOffset);
}
@Override
public void setClientData(@Nullable Object clientData) {
mClientData = clientData;
}
@Override
@Nullable
public Object getClientData() {
return mClientData;
}
}
public static class ResourceItemHandle implements Handle {
private final ResourceItem mItem;
public ResourceItemHandle(@NonNull ResourceItem item) {
mItem = item;
}
@NonNull
@Override
public Location resolve() {
// TODO: Look up the exact item location more
// closely
ResourceFile source = mItem.getSource();
assert source != null : mItem;
return create(source.getFile());
}
@Override
public void setClientData(@Nullable Object clientData) {
}
@Nullable
@Override
public Object getClientData() {
return null;
}
}
/**
* Whether to look forwards, or backwards, or in both directions, when
* searching for a pattern in the source code to determine the right
* position range for a given symbol.
* <p>
* When dealing with bytecode for example, there are only line number entries
* within method bodies, so when searching for the method declaration, we should only
* search backwards from the first line entry in the method.
*/
public enum SearchDirection {
/** Only search forwards */
FORWARD,
/** Only search backwards */
BACKWARD,
/** Search backwards from the current end of line (normally it's the beginning of
* the current line) */
EOL_BACKWARD,
/**
* Search both forwards and backwards from the given line, and prefer
* the match that is closest
*/
NEAREST,
}
/**
* Extra information pertaining to finding a symbol in a source buffer,
* used by {@link Location#create(File, String, int, String, String, SearchHints)}
*/
public static class SearchHints {
/**
* the direction to search for the nearest match in (provided
* {@code patternStart} is non null)
*/
@NonNull
private final SearchDirection mDirection;
/** Whether the matched pattern should be a whole word */
private boolean mWholeWord;
/**
* Whether the matched pattern should be a Java symbol (so for example,
* a match inside a comment or string literal should not be used)
*/
private boolean mJavaSymbol;
/**
* Whether the matched pattern corresponds to a constructor; if so, look for
* some other possible source aliases too, such as "super".
*/
private boolean mConstructor;
private SearchHints(@NonNull SearchDirection direction) {
super();
mDirection = direction;
}
/**
* Constructs a new {@link SearchHints} object
*
* @param direction the direction to search in for the pattern
* @return a new @link SearchHints} object
*/
@NonNull
public static SearchHints create(@NonNull SearchDirection direction) {
return new SearchHints(direction);
}
/**
* Indicates that pattern matches should apply to whole words only
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchWholeWord() {
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for whole words only */
public boolean isWholeWord() {
return mWholeWord;
}
/**
* Indicates that pattern matches should apply to Java symbols only
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchJavaSymbol() {
mJavaSymbol = true;
mWholeWord = true;
return this;
}
/** @return true if the pattern match should be for Java symbols only */
public boolean isJavaSymbol() {
return mJavaSymbol;
}
/**
* Indicates that pattern matches should apply to constructors. If so, look for
* some other possible source aliases too, such as "super".
*
* @return this, for constructor chaining
*/
@NonNull
public SearchHints matchConstructor() {
mConstructor = true;
mWholeWord = true;
mJavaSymbol = true;
return this;
}
/** @return true if the pattern match should be for a constructor */
public boolean isConstructor() {
return mConstructor;
}
}
}