• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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