Home | History | Annotate | Download | only in api
      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      */
     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      */
    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      */
    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      */
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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 
    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
    366         public List<File> getSourceFolders() {
    367             return mSourceFolders;
    368         }
    369 
    370         @NonNull
    371         public List<File> getClassFolders() {
    372             return mClassFolders;
    373         }
    374 
    375         @NonNull
    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
    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
    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
    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      */
    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