/* * 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; } } }