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 com.android.annotations.NonNull; 20 import com.android.annotations.Nullable; 21 import com.android.tools.lint.detector.api.Context; 22 import com.android.tools.lint.detector.api.Detector; 23 import com.android.tools.lint.detector.api.Issue; 24 import com.android.tools.lint.detector.api.Location; 25 import com.android.tools.lint.detector.api.Project; 26 import com.android.tools.lint.detector.api.Severity; 27 import com.google.common.annotations.Beta; 28 29 import org.w3c.dom.Document; 30 import org.w3c.dom.Element; 31 import org.w3c.dom.NodeList; 32 import org.xml.sax.InputSource; 33 34 import java.io.File; 35 import java.io.IOException; 36 import java.io.StringReader; 37 import java.net.URL; 38 import java.util.ArrayList; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.Map; 42 43 import javax.xml.parsers.DocumentBuilder; 44 import javax.xml.parsers.DocumentBuilderFactory; 45 46 /** 47 * Information about the tool embedding the lint analyzer. IDEs and other tools 48 * implementing lint support will extend this to integrate logging, displaying errors, 49 * etc. 50 * <p/> 51 * <b>NOTE: This is not a public or final API; if you rely on this be prepared 52 * to adjust your code for the next tools release.</b> 53 */ 54 @Beta 55 public abstract class LintClient { 56 57 private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$ 58 59 /** 60 * Returns a configuration for use by the given project. The configuration 61 * provides information about which issues are enabled, any customizations 62 * to the severity of an issue, etc. 63 * <p> 64 * By default this method returns a {@link DefaultConfiguration}. 65 * 66 * @param project the project to obtain a configuration for 67 * @return a configuration, never null. 68 */ 69 public Configuration getConfiguration(@NonNull Project project) { 70 return DefaultConfiguration.create(this, project, null); 71 } 72 73 /** 74 * Report the given issue. This method will only be called if the configuration 75 * provided by {@link #getConfiguration(Project)} has reported the corresponding 76 * issue as enabled and has not filtered out the issue with its 77 * {@link Configuration#ignore(Context, Issue, Location, String, Object)} method. 78 * <p> 79 * 80 * @param context the context used by the detector when the issue was found 81 * @param issue the issue that was found 82 * @param severity the severity of the issue 83 * @param location the location of the issue 84 * @param message the associated user message 85 * @param data optional extra data for a discovered issue, or null. The 86 * content depends on the specific issue. Detectors can pass 87 * extra info here which automatic fix tools etc can use to 88 * extract relevant information instead of relying on parsing the 89 * error message text. See each detector for details on which 90 * data if any is supplied for a given issue. 91 */ 92 public abstract void report( 93 @NonNull Context context, 94 @NonNull Issue issue, 95 @NonNull Severity severity, 96 @Nullable Location location, 97 @NonNull String message, 98 @Nullable Object data); 99 100 /** 101 * Send an exception or error message (with warning severity) to the log 102 * 103 * @param exception the exception, possibly null 104 * @param format the error message using {@link String#format} syntax, possibly null 105 * (though in that case the exception should not be null) 106 * @param args any arguments for the format string 107 */ 108 public void log( 109 @Nullable Throwable exception, 110 @Nullable String format, 111 @Nullable Object... args) { 112 log(Severity.WARNING, exception, format, args); 113 } 114 115 /** 116 * Send an exception or error message to the log 117 * 118 * @param severity the severity of the warning 119 * @param exception the exception, possibly null 120 * @param format the error message using {@link String#format} syntax, possibly null 121 * (though in that case the exception should not be null) 122 * @param args any arguments for the format string 123 */ 124 public abstract void log( 125 @NonNull Severity severity, 126 @Nullable Throwable exception, 127 @Nullable String format, 128 @Nullable Object... args); 129 130 /** 131 * Returns a {@link IDomParser} to use to parse XML 132 * 133 * @return a new {@link IDomParser}, or null if this client does not support 134 * XML analysis 135 */ 136 @Nullable 137 public abstract IDomParser getDomParser(); 138 139 /** 140 * Returns a {@link IJavaParser} to use to parse Java 141 * 142 * @return a new {@link IJavaParser}, or null if this client does not 143 * support Java analysis 144 */ 145 @Nullable 146 public abstract IJavaParser getJavaParser(); 147 148 /** 149 * Returns an optimal detector, if applicable. By default, just returns the 150 * original detector, but tools can replace detectors using this hook with a version 151 * that takes advantage of native capabilities of the tool. 152 * 153 * @param detectorClass the class of the detector to be replaced 154 * @return the new detector class, or just the original detector (not null) 155 */ 156 @NonNull 157 public Class<? extends Detector> replaceDetector( 158 @NonNull Class<? extends Detector> detectorClass) { 159 return detectorClass; 160 } 161 162 /** 163 * Reads the given text file and returns the content as a string 164 * 165 * @param file the file to read 166 * @return the string to return, never null (will be empty if there is an 167 * I/O error) 168 */ 169 @NonNull 170 public abstract String readFile(@NonNull File file); 171 172 /** 173 * Returns the list of source folders for Java source files 174 * 175 * @param project the project to look up Java source file locations for 176 * @return a list of source folders to search for .java files 177 */ 178 @NonNull 179 public List<File> getJavaSourceFolders(@NonNull Project project) { 180 return getEclipseClasspath(project, "src", "src", "gen"); //$NON-NLS-1$ //$NON-NLS-2$ 181 } 182 183 /** 184 * Returns the list of output folders for class files 185 * 186 * @param project the project to look up class file locations for 187 * @return a list of output folders to search for .class files 188 */ 189 @NonNull 190 public List<File> getJavaClassFolders(@NonNull Project project) { 191 return getEclipseClasspath(project, "output", "bin"); //$NON-NLS-1$ //$NON-NLS-2$ 192 } 193 194 /** 195 * Returns the list of Java libraries 196 * 197 * @param project the project to look up jar dependencies for 198 * @return a list of jar dependencies containing .class files 199 */ 200 @NonNull 201 public List<File> getJavaLibraries(@NonNull Project project) { 202 return getEclipseClasspath(project, "lib"); //$NON-NLS-1$ 203 } 204 205 /** 206 * Returns the {@link SdkInfo} to use for the given project. 207 * 208 * @param project the project to look up an {@link SdkInfo} for 209 * @return an {@link SdkInfo} for the project 210 */ 211 @NonNull 212 public SdkInfo getSdkInfo(@NonNull Project project) { 213 // By default no per-platform SDK info 214 return new DefaultSdkInfo(); 215 } 216 217 /** 218 * Returns a suitable location for storing cache files. Note that the 219 * directory may not exist. 220 * 221 * @param create if true, attempt to create the cache dir if it does not 222 * exist 223 * @return a suitable location for storing cache files, which may be null if 224 * the create flag was false, or if for some reason the directory 225 * could not be created 226 */ 227 @NonNull 228 public File getCacheDir(boolean create) { 229 String home = System.getProperty("user.home"); 230 String relative = ".android" + File.separator + "cache"; //$NON-NLS-1$ //$NON-NLS-2$ 231 File dir = new File(home, relative); 232 if (create && !dir.exists()) { 233 if (!dir.mkdirs()) { 234 return null; 235 } 236 } 237 return dir; 238 } 239 240 /** 241 * Returns the File corresponding to the system property or the environment variable 242 * for {@link #PROP_BIN_DIR}. 243 * This property is typically set by the SDK/tools/lint[.bat] wrapper. 244 * It denotes the path of the wrapper on disk. 245 * 246 * @return A new File corresponding to {@link LintClient#PROP_BIN_DIR} or null. 247 */ 248 @Nullable 249 private File getLintBinDir() { 250 // First check the Java properties (e.g. set using "java -jar ... -Dname=value") 251 String path = System.getProperty(PROP_BIN_DIR); 252 if (path == null || path.length() == 0) { 253 // If not found, check environment variables. 254 path = System.getenv(PROP_BIN_DIR); 255 } 256 if (path != null && path.length() > 0) { 257 return new File(path); 258 } 259 return null; 260 } 261 262 /** 263 * Locates an SDK resource (relative to the SDK root directory). 264 * <p> 265 * TODO: Consider switching to a {@link URL} return type instead. 266 * 267 * @param relativePath A relative path (using {@link File#separator} to 268 * separate path components) to the given resource 269 * @return a {@link File} pointing to the resource, or null if it does not 270 * exist 271 */ 272 @Nullable 273 public File findResource(@NonNull String relativePath) { 274 File dir = getLintBinDir(); 275 if (dir == null) { 276 throw new IllegalArgumentException("Lint must be invoked with the System property " 277 + PROP_BIN_DIR + " pointing to the ANDROID_SDK tools directory"); 278 } 279 280 File top = dir.getParentFile(); 281 File file = new File(top, relativePath); 282 if (file.exists()) { 283 return file; 284 } else { 285 return null; 286 } 287 } 288 289 /** 290 * Considers the given directory as an Eclipse project and returns either 291 * its source or its output folders depending on the {@code attribute} parameter. 292 */ 293 @NonNull 294 private List<File> getEclipseClasspath(@NonNull Project project, @NonNull String attribute, 295 @NonNull String... fallbackPaths) { 296 List<File> folders = new ArrayList<File>(); 297 File projectDir = project.getDir(); 298 File classpathFile = new File(projectDir, ".classpath"); //$NON-NLS-1$ 299 if (classpathFile.exists()) { 300 String classpathXml = readFile(classpathFile); 301 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 302 InputSource is = new InputSource(new StringReader(classpathXml)); 303 factory.setNamespaceAware(false); 304 factory.setValidating(false); 305 try { 306 DocumentBuilder builder = factory.newDocumentBuilder(); 307 Document document = builder.parse(is); 308 NodeList tags = document.getElementsByTagName("classpathentry"); //$NON-NLS-1$ 309 for (int i = 0, n = tags.getLength(); i < n; i++) { 310 Element element = (Element) tags.item(i); 311 String kind = element.getAttribute("kind"); //$NON-NLS-1$ 312 if (kind.equals(attribute)) { 313 String path = element.getAttribute("path"); //$NON-NLS-1$ 314 File sourceFolder = new File(projectDir, path); 315 if (sourceFolder.exists()) { 316 folders.add(sourceFolder); 317 } 318 } 319 } 320 } catch (Exception e) { 321 log(null, null); 322 } 323 } 324 325 // Fallback? 326 if (folders.size() == 0) { 327 for (String fallbackPath : fallbackPaths) { 328 File folder = new File(projectDir, fallbackPath); 329 if (folder.exists()) { 330 folders.add(folder); 331 } 332 } 333 } 334 335 return folders; 336 } 337 338 /** 339 * A map from directory to existing projects, or null. Used to ensure that 340 * projects are unique for a directory (in case we process a library project 341 * before its including project for example) 342 */ 343 private Map<File, Project> mDirToProject; 344 345 /** 346 * Returns a project for the given directory. This should return the same 347 * project for the same directory if called repeatedly. 348 * 349 * @param dir the directory containing the project 350 * @param referenceDir See {@link Project#getReferenceDir()}. 351 * @return a project, never null 352 */ 353 @NonNull 354 public Project getProject(@NonNull File dir, @NonNull File referenceDir) { 355 if (mDirToProject == null) { 356 mDirToProject = new HashMap<File, Project>(); 357 } 358 359 File canonicalDir = dir; 360 try { 361 // Attempt to use the canonical handle for the file, in case there 362 // are symlinks etc present (since when handling library projects, 363 // we also call getCanonicalFile to compute the result of appending 364 // relative paths, which can then resolve symlinks and end up with 365 // a different prefix) 366 canonicalDir = dir.getCanonicalFile(); 367 } catch (IOException ioe) { 368 // pass 369 } 370 371 Project project = mDirToProject.get(canonicalDir); 372 if (project != null) { 373 return project; 374 } 375 376 377 project = Project.create(this, dir, referenceDir); 378 mDirToProject.put(canonicalDir, project); 379 return project; 380 } 381 } 382