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.client.api; 18 19 import static com.android.SdkConstants.CLASS_FOLDER; 20 import static com.android.SdkConstants.DOT_JAR; 21 import static com.android.SdkConstants.GEN_FOLDER; 22 import static com.android.SdkConstants.LIBS_FOLDER; 23 import static com.android.SdkConstants.SRC_FOLDER; 24 25 import com.android.SdkConstants; 26 import com.android.annotations.NonNull; 27 import com.android.annotations.Nullable; 28 import com.android.sdklib.IAndroidTarget; 29 import com.android.sdklib.SdkManager; 30 import com.android.tools.lint.detector.api.Context; 31 import com.android.tools.lint.detector.api.Detector; 32 import com.android.tools.lint.detector.api.Issue; 33 import com.android.tools.lint.detector.api.LintUtils; 34 import com.android.tools.lint.detector.api.Location; 35 import com.android.tools.lint.detector.api.Project; 36 import com.android.tools.lint.detector.api.Severity; 37 import com.android.utils.StdLogger; 38 import com.android.utils.StdLogger.Level; 39 import com.google.common.annotations.Beta; 40 import com.google.common.collect.Maps; 41 import com.google.common.io.Files; 42 43 import org.w3c.dom.Document; 44 import org.w3c.dom.Element; 45 import org.w3c.dom.NodeList; 46 import org.xml.sax.InputSource; 47 48 import java.io.File; 49 import java.io.IOException; 50 import java.io.StringReader; 51 import java.net.URL; 52 import java.util.ArrayList; 53 import java.util.HashMap; 54 import java.util.List; 55 import java.util.Map; 56 57 import javax.xml.parsers.DocumentBuilder; 58 import javax.xml.parsers.DocumentBuilderFactory; 59 60 /** 61 * Information about the tool embedding the lint analyzer. IDEs and other tools 62 * implementing lint support will extend this to integrate logging, displaying errors, 63 * etc. 64 * <p/> 65 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 66 * to adjust your code for the next tools release.</b> 67 */ 68 @Beta 69 public abstract class LintClient { 70 private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$ 71 72 /** 73 * Returns a configuration for use by the given project. The configuration 74 * provides information about which issues are enabled, any customizations 75 * to the severity of an issue, etc. 76 * <p> 77 * By default this method returns a {@link DefaultConfiguration}. 78 * 79 * @param project the project to obtain a configuration for 80 * @return a configuration, never null. 81 */ getConfiguration(@onNull Project project)82 public Configuration getConfiguration(@NonNull Project project) { 83 return DefaultConfiguration.create(this, project, null); 84 } 85 86 /** 87 * Report the given issue. This method will only be called if the configuration 88 * provided by {@link #getConfiguration(Project)} has reported the corresponding 89 * issue as enabled and has not filtered out the issue with its 90 * {@link Configuration#ignore(Context, Issue, Location, String, Object)} method. 91 * <p> 92 * 93 * @param context the context used by the detector when the issue was found 94 * @param issue the issue that was found 95 * @param severity the severity of the issue 96 * @param location the location of the issue 97 * @param message the associated user message 98 * @param data optional extra data for a discovered issue, or null. The 99 * content depends on the specific issue. Detectors can pass 100 * extra info here which automatic fix tools etc can use to 101 * extract relevant information instead of relying on parsing the 102 * error message text. See each detector for details on which 103 * data if any is supplied for a given issue. 104 */ report( @onNull Context context, @NonNull Issue issue, @NonNull Severity severity, @Nullable Location location, @NonNull String message, @Nullable Object data)105 public abstract void report( 106 @NonNull Context context, 107 @NonNull Issue issue, 108 @NonNull Severity severity, 109 @Nullable Location location, 110 @NonNull String message, 111 @Nullable Object data); 112 113 /** 114 * Send an exception or error message (with warning severity) to the log 115 * 116 * @param exception the exception, possibly null 117 * @param format the error message using {@link String#format} syntax, possibly null 118 * (though in that case the exception should not be null) 119 * @param args any arguments for the format string 120 */ log( @ullable Throwable exception, @Nullable String format, @Nullable Object... args)121 public void log( 122 @Nullable Throwable exception, 123 @Nullable String format, 124 @Nullable Object... args) { 125 log(Severity.WARNING, exception, format, args); 126 } 127 128 /** 129 * Send an exception or error message to the log 130 * 131 * @param severity the severity of the warning 132 * @param exception the exception, possibly null 133 * @param format the error message using {@link String#format} syntax, possibly null 134 * (though in that case the exception should not be null) 135 * @param args any arguments for the format string 136 */ log( @onNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args)137 public abstract void log( 138 @NonNull Severity severity, 139 @Nullable Throwable exception, 140 @Nullable String format, 141 @Nullable Object... args); 142 143 /** 144 * Returns a {@link IDomParser} to use to parse XML 145 * 146 * @return a new {@link IDomParser}, or null if this client does not support 147 * XML analysis 148 */ 149 @Nullable getDomParser()150 public abstract IDomParser getDomParser(); 151 152 /** 153 * Returns a {@link IJavaParser} to use to parse Java 154 * 155 * @return a new {@link IJavaParser}, or null if this client does not 156 * support Java analysis 157 */ 158 @Nullable getJavaParser()159 public abstract IJavaParser getJavaParser(); 160 161 /** 162 * Returns an optimal detector, if applicable. By default, just returns the 163 * original detector, but tools can replace detectors using this hook with a version 164 * that takes advantage of native capabilities of the tool. 165 * 166 * @param detectorClass the class of the detector to be replaced 167 * @return the new detector class, or just the original detector (not null) 168 */ 169 @NonNull replaceDetector( @onNull Class<? extends Detector> detectorClass)170 public Class<? extends Detector> replaceDetector( 171 @NonNull Class<? extends Detector> detectorClass) { 172 return detectorClass; 173 } 174 175 /** 176 * Reads the given text file and returns the content as a string 177 * 178 * @param file the file to read 179 * @return the string to return, never null (will be empty if there is an 180 * I/O error) 181 */ 182 @NonNull readFile(@onNull File file)183 public abstract String readFile(@NonNull File file); 184 185 /** 186 * Reads the given binary file and returns the content as a byte array. 187 * By default this method will read the bytes from the file directly, 188 * but this can be customized by a client if for example I/O could be 189 * held in memory and not flushed to disk yet. 190 * 191 * @param file the file to read 192 * @return the bytes in the file, never null 193 * @throws IOException if the file does not exist, or if the file cannot be 194 * read for some reason 195 */ 196 @NonNull readBytes(@onNull File file)197 public byte[] readBytes(@NonNull File file) throws IOException { 198 return Files.toByteArray(file); 199 } 200 201 /** 202 * Returns the list of source folders for Java source files 203 * 204 * @param project the project to look up Java source file locations for 205 * @return a list of source folders to search for .java files 206 */ 207 @NonNull getJavaSourceFolders(@onNull Project project)208 public List<File> getJavaSourceFolders(@NonNull Project project) { 209 return getClassPath(project).getSourceFolders(); 210 } 211 212 /** 213 * Returns the list of output folders for class files 214 * 215 * @param project the project to look up class file locations for 216 * @return a list of output folders to search for .class files 217 */ 218 @NonNull getJavaClassFolders(@onNull Project project)219 public List<File> getJavaClassFolders(@NonNull Project project) { 220 return getClassPath(project).getClassFolders(); 221 222 } 223 224 /** 225 * Returns the list of Java libraries 226 * 227 * @param project the project to look up jar dependencies for 228 * @return a list of jar dependencies containing .class files 229 */ 230 @NonNull getJavaLibraries(@onNull Project project)231 public List<File> getJavaLibraries(@NonNull Project project) { 232 return getClassPath(project).getLibraries(); 233 } 234 235 /** 236 * Returns the {@link SdkInfo} to use for the given project. 237 * 238 * @param project the project to look up an {@link SdkInfo} for 239 * @return an {@link SdkInfo} for the project 240 */ 241 @NonNull getSdkInfo(@onNull Project project)242 public SdkInfo getSdkInfo(@NonNull Project project) { 243 // By default no per-platform SDK info 244 return new DefaultSdkInfo(); 245 } 246 247 /** 248 * Returns a suitable location for storing cache files. Note that the 249 * directory may not exist. 250 * 251 * @param create if true, attempt to create the cache dir if it does not 252 * exist 253 * @return a suitable location for storing cache files, which may be null if 254 * the create flag was false, or if for some reason the directory 255 * could not be created 256 */ 257 @Nullable getCacheDir(boolean create)258 public File getCacheDir(boolean create) { 259 String home = System.getProperty("user.home"); 260 String relative = ".android" + File.separator + "cache"; //$NON-NLS-1$ //$NON-NLS-2$ 261 File dir = new File(home, relative); 262 if (create && !dir.exists()) { 263 if (!dir.mkdirs()) { 264 return null; 265 } 266 } 267 return dir; 268 } 269 270 /** 271 * Returns the File corresponding to the system property or the environment variable 272 * for {@link #PROP_BIN_DIR}. 273 * This property is typically set by the SDK/tools/lint[.bat] wrapper. 274 * It denotes the path of the wrapper on disk. 275 * 276 * @return A new File corresponding to {@link LintClient#PROP_BIN_DIR} or null. 277 */ 278 @Nullable getLintBinDir()279 private File getLintBinDir() { 280 // First check the Java properties (e.g. set using "java -jar ... -Dname=value") 281 String path = System.getProperty(PROP_BIN_DIR); 282 if (path == null || path.length() == 0) { 283 // If not found, check environment variables. 284 path = System.getenv(PROP_BIN_DIR); 285 } 286 if (path != null && path.length() > 0) { 287 return new File(path); 288 } 289 return null; 290 } 291 292 /** 293 * Returns the File pointing to the user's SDK install area. This is generally 294 * the root directory containing the lint tool (but also platforms/ etc). 295 * 296 * @return a file pointing to the user's install area 297 */ 298 @Nullable getSdkHome()299 public File getSdkHome() { 300 File binDir = getLintBinDir(); 301 if (binDir != null) { 302 assert binDir.getName().equals("tools"); 303 304 File root = binDir.getParentFile(); 305 if (root != null && root.isDirectory()) { 306 return root; 307 } 308 } 309 310 String home = System.getenv("ANDROID_HOME"); //$NON-NLS-1$ 311 if (home != null) { 312 return new File(home); 313 } 314 315 return null; 316 } 317 318 /** 319 * Locates an SDK resource (relative to the SDK root directory). 320 * <p> 321 * TODO: Consider switching to a {@link URL} return type instead. 322 * 323 * @param relativePath A relative path (using {@link File#separator} to 324 * separate path components) to the given resource 325 * @return a {@link File} pointing to the resource, or null if it does not 326 * exist 327 */ 328 @Nullable findResource(@onNull String relativePath)329 public File findResource(@NonNull String relativePath) { 330 File dir = getLintBinDir(); 331 if (dir == null) { 332 throw new IllegalArgumentException("Lint must be invoked with the System property " 333 + PROP_BIN_DIR + " pointing to the ANDROID_SDK tools directory"); 334 } 335 336 File top = dir.getParentFile(); 337 File file = new File(top, relativePath); 338 if (file.exists()) { 339 return file; 340 } else { 341 return null; 342 } 343 } 344 345 private Map<Project, ClassPathInfo> mProjectInfo; 346 347 /** 348 * Information about class paths (sources, class files and libraries) 349 * usually associated with a project. 350 */ 351 protected static class ClassPathInfo { 352 private final List<File> mClassFolders; 353 private final List<File> mSourceFolders; 354 private final List<File> mLibraries; 355 ClassPathInfo( @onNull List<File> sourceFolders, @NonNull List<File> classFolders, @NonNull List<File> libraries)356 public ClassPathInfo( 357 @NonNull List<File> sourceFolders, 358 @NonNull List<File> classFolders, 359 @NonNull List<File> libraries) { 360 mSourceFolders = sourceFolders; 361 mClassFolders = classFolders; 362 mLibraries = libraries; 363 } 364 365 @NonNull getSourceFolders()366 public List<File> getSourceFolders() { 367 return mSourceFolders; 368 } 369 370 @NonNull getClassFolders()371 public List<File> getClassFolders() { 372 return mClassFolders; 373 } 374 375 @NonNull getLibraries()376 public List<File> getLibraries() { 377 return mLibraries; 378 } 379 } 380 381 /** 382 * Considers the given project as an Eclipse project and returns class path 383 * information for the project - the source folder(s), the output folder and 384 * any libraries. 385 * <p> 386 * Callers will not cache calls to this method, so if it's expensive to compute 387 * the classpath info, this method should perform its own caching. 388 * 389 * @param project the project to look up class path info for 390 * @return a class path info object, never null 391 */ 392 @NonNull getClassPath(@onNull Project project)393 protected ClassPathInfo getClassPath(@NonNull Project project) { 394 ClassPathInfo info; 395 if (mProjectInfo == null) { 396 mProjectInfo = Maps.newHashMap(); 397 info = null; 398 } else { 399 info = mProjectInfo.get(project); 400 } 401 402 if (info == null) { 403 List<File> sources = new ArrayList<File>(2); 404 List<File> classes = new ArrayList<File>(1); 405 List<File> libraries = new ArrayList<File>(); 406 407 File projectDir = project.getDir(); 408 File classpathFile = new File(projectDir, ".classpath"); //$NON-NLS-1$ 409 if (classpathFile.exists()) { 410 String classpathXml = readFile(classpathFile); 411 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 412 InputSource is = new InputSource(new StringReader(classpathXml)); 413 factory.setNamespaceAware(false); 414 factory.setValidating(false); 415 try { 416 DocumentBuilder builder = factory.newDocumentBuilder(); 417 Document document = builder.parse(is); 418 NodeList tags = document.getElementsByTagName("classpathentry"); //$NON-NLS-1$ 419 for (int i = 0, n = tags.getLength(); i < n; i++) { 420 Element element = (Element) tags.item(i); 421 String kind = element.getAttribute("kind"); //$NON-NLS-1$ 422 List<File> addTo = null; 423 if (kind.equals("src")) { //$NON-NLS-1$ 424 addTo = sources; 425 } else if (kind.equals("output")) { //$NON-NLS-1$ 426 addTo = classes; 427 } else if (kind.equals("lib")) { //$NON-NLS-1$ 428 addTo = libraries; 429 } 430 if (addTo != null) { 431 String path = element.getAttribute("path"); //$NON-NLS-1$ 432 File folder = new File(projectDir, path); 433 if (folder.exists()) { 434 addTo.add(folder); 435 } 436 } 437 } 438 } catch (Exception e) { 439 log(null, null); 440 } 441 } 442 443 // Add in libraries that aren't specified in the .classpath file 444 File libs = new File(project.getDir(), LIBS_FOLDER); 445 if (libs.isDirectory()) { 446 File[] jars = libs.listFiles(); 447 if (jars != null) { 448 for (File jar : jars) { 449 if (LintUtils.endsWith(jar.getPath(), DOT_JAR) 450 && !libraries.contains(jar)) { 451 libraries.add(jar); 452 } 453 } 454 } 455 } 456 457 if (classes.size() == 0) { 458 File folder = new File(projectDir, CLASS_FOLDER); 459 if (folder.exists()) { 460 classes.add(folder); 461 } else { 462 // Maven checks 463 folder = new File(projectDir, 464 "target" + File.separator + "classes"); //$NON-NLS-1$ //$NON-NLS-2$ 465 if (folder.exists()) { 466 classes.add(folder); 467 468 // If it's maven, also correct the source path, "src" works but 469 // it's in a more specific subfolder 470 if (sources.size() == 0) { 471 File src = new File(projectDir, 472 "src" + File.separator //$NON-NLS-1$ 473 + "main" + File.separator //$NON-NLS-1$ 474 + "java"); //$NON-NLS-1$ 475 if (src.exists()) { 476 sources.add(src); 477 } else { 478 src = new File(projectDir, SRC_FOLDER); 479 if (src.exists()) { 480 sources.add(src); 481 } 482 } 483 484 File gen = new File(projectDir, 485 "target" + File.separator //$NON-NLS-1$ 486 + "generated-sources" + File.separator //$NON-NLS-1$ 487 + "r"); //$NON-NLS-1$ 488 if (gen.exists()) { 489 sources.add(gen); 490 } 491 } 492 } 493 } 494 } 495 496 // Fallback, in case there is no Eclipse project metadata here 497 if (sources.size() == 0) { 498 File src = new File(projectDir, SRC_FOLDER); 499 if (src.exists()) { 500 sources.add(src); 501 } 502 File gen = new File(projectDir, GEN_FOLDER); 503 if (gen.exists()) { 504 sources.add(gen); 505 } 506 } 507 508 info = new ClassPathInfo(sources, classes, libraries); 509 mProjectInfo.put(project, info); 510 } 511 512 return info; 513 } 514 515 /** 516 * A map from directory to existing projects, or null. Used to ensure that 517 * projects are unique for a directory (in case we process a library project 518 * before its including project for example) 519 */ 520 private Map<File, Project> mDirToProject; 521 522 /** 523 * Returns a project for the given directory. This should return the same 524 * project for the same directory if called repeatedly. 525 * 526 * @param dir the directory containing the project 527 * @param referenceDir See {@link Project#getReferenceDir()}. 528 * @return a project, never null 529 */ 530 @NonNull getProject(@onNull File dir, @NonNull File referenceDir)531 public Project getProject(@NonNull File dir, @NonNull File referenceDir) { 532 if (mDirToProject == null) { 533 mDirToProject = new HashMap<File, Project>(); 534 } 535 536 File canonicalDir = dir; 537 try { 538 // Attempt to use the canonical handle for the file, in case there 539 // are symlinks etc present (since when handling library projects, 540 // we also call getCanonicalFile to compute the result of appending 541 // relative paths, which can then resolve symlinks and end up with 542 // a different prefix) 543 canonicalDir = dir.getCanonicalFile(); 544 } catch (IOException ioe) { 545 // pass 546 } 547 548 Project project = mDirToProject.get(canonicalDir); 549 if (project != null) { 550 return project; 551 } 552 553 554 project = Project.create(this, dir, referenceDir); 555 mDirToProject.put(canonicalDir, project); 556 return project; 557 } 558 559 private IAndroidTarget[] mTargets; 560 561 /** 562 * Returns all the {@link IAndroidTarget} versions installed in the user's SDK install 563 * area. 564 * 565 * @return all the installed targets 566 */ 567 @NonNull getTargets()568 public IAndroidTarget[] getTargets() { 569 if (mTargets == null) { 570 File sdkHome = getSdkHome(); 571 if (sdkHome != null) { 572 StdLogger log = new StdLogger(Level.WARNING); 573 SdkManager manager = SdkManager.createManager(sdkHome.getPath(), log); 574 mTargets = manager.getTargets(); 575 } else { 576 mTargets = new IAndroidTarget[0]; 577 } 578 } 579 580 return mTargets; 581 } 582 583 /** 584 * Returns the highest known API level. 585 * 586 * @return the highest known API level 587 */ getHighestKnownApiLevel()588 public int getHighestKnownApiLevel() { 589 int max = SdkConstants.HIGHEST_KNOWN_API; 590 591 for (IAndroidTarget target : getTargets()) { 592 if (target.isPlatform()) { 593 int api = target.getVersion().getApiLevel(); 594 if (api > max && !target.getVersion().isPreview()) { 595 max = api; 596 } 597 } 598 } 599 600 return max; 601 } 602 } 603