• 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 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      */
getConfiguration(@onNull Project project)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      */
report( @onNull Context context, @NonNull Issue issue, @NonNull Severity severity, @Nullable Location location, @NonNull String message, @Nullable Object data)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      */
log( @ullable Throwable exception, @Nullable String format, @Nullable Object... args)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      */
log( @onNull Severity severity, @Nullable Throwable exception, @Nullable String format, @Nullable Object... args)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
getDomParser()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
getJavaParser()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
replaceDetector( @onNull Class<? extends Detector> detectorClass)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
readFile(@onNull File file)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
getJavaSourceFolders(@onNull Project project)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
getJavaClassFolders(@onNull Project project)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
getJavaLibraries(@onNull Project project)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
getSdkInfo(@onNull Project project)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
getCacheDir(boolean create)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
getLintBinDir()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
findResource(@onNull String relativePath)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
getEclipseClasspath(@onNull Project project, @NonNull String attribute, @NonNull String... fallbackPaths)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
getProject(@onNull File dir, @NonNull File referenceDir)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