1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tools.lint.detector.api; 18 19 import com.android.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.google.common.annotations.Beta; 22 23 import java.io.File; 24 25 /** 26 * Location information for a warning 27 * <p/> 28 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 29 * to adjust your code for the next tools release.</b> 30 */ 31 @Beta 32 public class Location { 33 private final File mFile; 34 private final Position mStart; 35 private final Position mEnd; 36 private String mMessage; 37 private Location mSecondary; 38 private Object mClientData; 39 40 /** 41 * (Private constructor, use one of the factory methods 42 * {@link Location#create(File)}, 43 * {@link Location#create(File, Position, Position)}, or 44 * {@link Location#create(File, String, int, int)}. 45 * <p> 46 * Constructs a new location range for the given file, from start to end. If 47 * the length of the range is not known, end may be null. 48 * 49 * @param file the associated file (but see the documentation for 50 * {@link #getFile()} for more information on what the file 51 * represents) 52 * @param start the starting position, or null 53 * @param end the ending position, or null 54 */ Location(@onNull File file, @Nullable Position start, @Nullable Position end)55 protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) { 56 super(); 57 this.mFile = file; 58 this.mStart = start; 59 this.mEnd = end; 60 } 61 62 /** 63 * Returns the file containing the warning. Note that the file *itself* may 64 * not yet contain the error. When editing a file in the IDE for example, 65 * the tool could generate warnings in the background even before the 66 * document is saved. However, the file is used as a identifying token for 67 * the document being edited, and the IDE integration can map this back to 68 * error locations in the editor source code. 69 * 70 * @return the file handle for the location 71 */ 72 @NonNull getFile()73 public File getFile() { 74 return mFile; 75 } 76 77 /** 78 * The start position of the range 79 * 80 * @return the start position of the range, or null 81 */ 82 @Nullable getStart()83 public Position getStart() { 84 return mStart; 85 } 86 87 /** 88 * The end position of the range 89 * 90 * @return the start position of the range, may be null for an empty range 91 */ 92 @Nullable getEnd()93 public Position getEnd() { 94 return mEnd; 95 } 96 97 /** 98 * Returns a secondary location associated with this location (if 99 * applicable), or null. 100 * 101 * @return a secondary location or null 102 */ 103 @Nullable getSecondary()104 public Location getSecondary() { 105 return mSecondary; 106 } 107 108 /** 109 * Sets a secondary location for this location. 110 * 111 * @param secondary a secondary location associated with this location 112 */ setSecondary(@onNull Location secondary)113 public void setSecondary(@NonNull Location secondary) { 114 this.mSecondary = secondary; 115 } 116 117 /** 118 * Sets a custom message for this location. This is typically used for 119 * secondary locations, to describe the significance of this alternate 120 * location. For example, for a duplicate id warning, the primary location 121 * might say "This is a duplicate id", pointing to the second occurrence of 122 * id declaration, and then the secondary location could point to the 123 * original declaration with the custom message "Originally defined here". 124 * 125 * @param message the message to apply to this location 126 */ setMessage(@onNull String message)127 public void setMessage(@NonNull String message) { 128 mMessage = message; 129 } 130 131 /** 132 * Returns the custom message for this location, if any. This is typically 133 * used for secondary locations, to describe the significance of this 134 * alternate location. For example, for a duplicate id warning, the primary 135 * location might say "This is a duplicate id", pointing to the second 136 * occurrence of id declaration, and then the secondary location could point 137 * to the original declaration with the custom message 138 * "Originally defined here". 139 * 140 * @return the custom message for this location, or null 141 */ 142 @Nullable getMessage()143 public String getMessage() { 144 return mMessage; 145 } 146 147 /** 148 * Sets the client data associated with this location. This is an optional 149 * field which can be used by the creator of the {@link Location} to store 150 * temporary state associated with the location. 151 * 152 * @param clientData the data to store with this location 153 */ setClientData(@ullable Object clientData)154 public void setClientData(@Nullable Object clientData) { 155 mClientData = clientData; 156 } 157 158 /** 159 * Returns the client data associated with this location - an optional field 160 * which can be used by the creator of the {@link Location} to store 161 * temporary state associated with the location. 162 * 163 * @return the data associated with this location 164 */ 165 @Nullable getClientData()166 public Object getClientData() { 167 return mClientData; 168 } 169 170 @Override toString()171 public String toString() { 172 return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message=" 173 + mMessage + "]"; 174 } 175 176 /** 177 * Creates a new location for the given file 178 * 179 * @param file the file to create a location for 180 * @return a new location 181 */ 182 @NonNull create(@onNull File file)183 public static Location create(@NonNull File file) { 184 return new Location(file, null /*start*/, null /*end*/); 185 } 186 187 /** 188 * Creates a new location for the given file and starting and ending 189 * positions. 190 * 191 * @param file the file containing the positions 192 * @param start the starting position 193 * @param end the ending position 194 * @return a new location 195 */ 196 @NonNull create( @onNull File file, @NonNull Position start, @NonNull Position end)197 public static Location create( 198 @NonNull File file, 199 @NonNull Position start, 200 @NonNull Position end) { 201 return new Location(file, start, end); 202 } 203 204 /** 205 * Creates a new location for the given file, with the given contents, for 206 * the given offset range. 207 * 208 * @param file the file containing the location 209 * @param contents the current contents of the file 210 * @param startOffset the starting offset 211 * @param endOffset the ending offset 212 * @return a new location 213 */ 214 @NonNull create( @onNull File file, @Nullable String contents, int startOffset, int endOffset)215 public static Location create( 216 @NonNull File file, 217 @Nullable String contents, 218 int startOffset, 219 int endOffset) { 220 if (startOffset < 0 || endOffset < startOffset) { 221 throw new IllegalArgumentException("Invalid offsets"); 222 } 223 224 if (contents == null) { 225 return new Location(file, 226 new DefaultPosition(-1, -1, startOffset), 227 new DefaultPosition(-1, -1, endOffset)); 228 } 229 230 int size = contents.length(); 231 endOffset = Math.min(endOffset, size); 232 startOffset = Math.min(startOffset, endOffset); 233 Position start = null; 234 int line = 0; 235 int lineOffset = 0; 236 for (int offset = 0; offset <= size; offset++) { 237 if (offset == startOffset) { 238 start = new DefaultPosition(line, offset - lineOffset, offset); 239 } 240 if (offset == endOffset) { 241 Position end = new DefaultPosition(line, offset - lineOffset, offset); 242 return new Location(file, start, end); 243 } 244 char c = contents.charAt(offset); 245 if (c == '\n') { 246 lineOffset = offset; 247 line++; 248 } 249 } 250 return Location.create(file); 251 } 252 253 /** 254 * Creates a new location for the given file, with the given contents, for 255 * the given line number. 256 * 257 * @param file the file containing the location 258 * @param contents the current contents of the file 259 * @param line the line number (0-based) for the position 260 * @return a new location 261 */ 262 @NonNull create(@onNull File file, @NonNull String contents, int line)263 public static Location create(@NonNull File file, @NonNull String contents, int line) { 264 return create(file, contents, line, null, null); 265 } 266 267 /** 268 * Creates a new location for the given file, with the given contents, for 269 * the given line number. 270 * 271 * @param file the file containing the location 272 * @param contents the current contents of the file 273 * @param line the line number (0-based) for the position 274 * @param patternStart an optional pattern to search for from the line 275 * match; if found, adjust the column and offsets to begin at the 276 * pattern start 277 * @param patternEnd an optional pattern to search for behind the start 278 * pattern; if found, adjust the end offset to match the end of 279 * the pattern 280 * @return a new location 281 */ 282 @NonNull create(@onNull File file, @NonNull String contents, int line, @Nullable String patternStart, @Nullable String patternEnd)283 public static Location create(@NonNull File file, @NonNull String contents, int line, 284 @Nullable String patternStart, @Nullable String patternEnd) { 285 int currentLine = 0; 286 int offset = 0; 287 while (currentLine < line) { 288 offset = contents.indexOf('\n', offset); 289 if (offset == -1) { 290 return Location.create(file); 291 } 292 currentLine++; 293 offset++; 294 } 295 296 if (line == currentLine) { 297 if (patternStart != null) { 298 int index = contents.indexOf(patternStart, offset); 299 if (index == -1) { 300 // Allow some flexibility: peek at previous couple of lines 301 // as well (for example, bytecode line numbers are sometimes 302 // a few lines off from their location in a source file 303 // since they are attached to executable lines of code) 304 int lineStart = offset; 305 for (int i = 0; i < 4; i++) { 306 int prevLineStart = contents.lastIndexOf('\n', lineStart - 1); 307 if (prevLineStart == -1) { 308 break; 309 } 310 index = contents.indexOf(patternStart, prevLineStart); 311 if (index != -1 || prevLineStart == 0) { 312 break; 313 } 314 lineStart = prevLineStart; 315 } 316 } 317 318 if (index != -1) { 319 int lineStart = contents.lastIndexOf('\n', index); 320 if (lineStart == -1) { 321 lineStart = 0; 322 } else { 323 lineStart++; // was pointing to the previous line's CR, not line start 324 } 325 int column = index - lineStart; 326 if (patternEnd != null) { 327 int end = contents.indexOf(patternEnd, offset + patternStart.length()); 328 if (end != -1) { 329 return new Location(file, new DefaultPosition(line, column, index), 330 new DefaultPosition(line, -1, end + patternEnd.length())); 331 } 332 } 333 return new Location(file, new DefaultPosition(line, column, index), 334 new DefaultPosition(line, column, index + patternStart.length())); 335 } 336 } 337 338 Position position = new DefaultPosition(line, -1, offset); 339 return new Location(file, position, position); 340 } 341 342 return Location.create(file); 343 } 344 345 /** 346 * Reverses the secondary location list initiated by the given location 347 * 348 * @param location the first location in the list 349 * @return the first location in the reversed list 350 */ reverse(Location location)351 public static Location reverse(Location location) { 352 Location next = location.getSecondary(); 353 location.setSecondary(null); 354 while (next != null) { 355 Location nextNext = next.getSecondary(); 356 next.setSecondary(location); 357 location = next; 358 next = nextNext; 359 } 360 361 return location; 362 } 363 364 /** 365 * A {@link Handle} is a reference to a location. The point of a location 366 * handle is to be able to create them cheaply, and then resolve them into 367 * actual locations later (if needed). This makes it possible to for example 368 * delay looking up line numbers, for locations that are offset based. 369 */ 370 public static interface Handle { 371 /** 372 * Compute a full location for the given handle 373 * 374 * @return create a location for this handle 375 */ 376 @NonNull resolve()377 Location resolve(); 378 379 /** 380 * Sets the client data associated with this location. This is an optional 381 * field which can be used by the creator of the {@link Location} to store 382 * temporary state associated with the location. 383 * 384 * @param clientData the data to store with this location 385 */ setClientData(@ullable Object clientData)386 public void setClientData(@Nullable Object clientData); 387 388 /** 389 * Returns the client data associated with this location - an optional field 390 * which can be used by the creator of the {@link Location} to store 391 * temporary state associated with the location. 392 * 393 * @return the data associated with this location 394 */ 395 @Nullable getClientData()396 public Object getClientData(); 397 } 398 399 /** A default {@link Handle} implementation for simple file offsets */ 400 public static class DefaultLocationHandle implements Handle { 401 private File mFile; 402 private String mContents; 403 private int mStartOffset; 404 private int mEndOffset; 405 private Object mClientData; 406 407 /** 408 * Constructs a new {@link DefaultLocationHandle} 409 * 410 * @param context the context pointing to the file and its contents 411 * @param startOffset the start offset within the file 412 * @param endOffset the end offset within the file 413 */ DefaultLocationHandle(@onNull Context context, int startOffset, int endOffset)414 public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) { 415 mFile = context.file; 416 mContents = context.getContents(); 417 mStartOffset = startOffset; 418 mEndOffset = endOffset; 419 } 420 421 @Override 422 @NonNull resolve()423 public Location resolve() { 424 return Location.create(mFile, mContents, mStartOffset, mEndOffset); 425 } 426 427 @Override setClientData(@ullable Object clientData)428 public void setClientData(@Nullable Object clientData) { 429 mClientData = clientData; 430 } 431 432 @Override 433 @Nullable getClientData()434 public Object getClientData() { 435 return mClientData; 436 } 437 } 438 } 439