Home | History | Annotate | Download | only in lint
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
      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 package com.android.ide.eclipse.adt.internal.lint;
     17 
     18 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML;
     19 
     20 import com.android.ide.eclipse.adt.AdtPlugin;
     21 import com.android.ide.eclipse.adt.AdtUtils;
     22 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     23 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     24 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     25 import com.android.sdklib.SdkConstants;
     26 import com.android.tools.lint.client.api.IssueRegistry;
     27 import com.android.tools.lint.client.api.LintDriver;
     28 import com.android.tools.lint.detector.api.Issue;
     29 import com.android.tools.lint.detector.api.Scope;
     30 
     31 import org.eclipse.core.resources.IFile;
     32 import org.eclipse.core.resources.IMarker;
     33 import org.eclipse.core.resources.IProject;
     34 import org.eclipse.core.resources.IResource;
     35 import org.eclipse.core.runtime.IProgressMonitor;
     36 import org.eclipse.core.runtime.IStatus;
     37 import org.eclipse.core.runtime.Status;
     38 import org.eclipse.core.runtime.jobs.IJobManager;
     39 import org.eclipse.core.runtime.jobs.Job;
     40 import org.eclipse.jface.dialogs.MessageDialog;
     41 import org.eclipse.jface.text.IDocument;
     42 import org.eclipse.swt.widgets.Shell;
     43 
     44 import java.io.File;
     45 import java.util.ArrayList;
     46 import java.util.Collections;
     47 import java.util.EnumSet;
     48 import java.util.IdentityHashMap;
     49 import java.util.List;
     50 import java.util.Map;
     51 
     52 /**
     53  * Eclipse implementation for running lint on workspace files and projects.
     54  */
     55 public class EclipseLintRunner {
     56     static final String MARKER_CHECKID_PROPERTY = "checkid";    //$NON-NLS-1$
     57 
     58     /**
     59      * Runs lint and updates the markers, and waits for the result. Returns
     60      * true if fatal errors were found.
     61      *
     62      * @param resources the resources (project, folder or file) to be analyzed
     63      * @param doc the associated document, if known, or null
     64      * @param fatalOnly if true, only report fatal issues (severity=error)
     65      * @return true if any fatal errors were encountered.
     66      */
     67     private static boolean runLint(List<? extends IResource> resources, IDocument doc,
     68             boolean fatalOnly) {
     69         resources = addLibraries(resources);
     70         CheckFileJob job = (CheckFileJob) startLint(resources, doc, fatalOnly, false /*show*/);
     71         try {
     72             job.join();
     73             boolean fatal = job.isFatal();
     74 
     75             if (fatal) {
     76                 LintViewPart.show(resources);
     77             }
     78 
     79             return fatal;
     80         } catch (InterruptedException e) {
     81             AdtPlugin.log(e, null);
     82         }
     83 
     84         return false;
     85     }
     86 
     87     /**
     88      * Runs lint and updates the markers. Does not wait for the job to
     89      * finish - just returns immediately.
     90      *
     91      * @param resources the resources (project, folder or file) to be analyzed
     92      * @param doc the associated document, if known, or null
     93      * @param fatalOnly if true, only report fatal issues (severity=error)
     94      * @param show if true, show the results in a {@link LintViewPart}
     95      * @return the job running lint in the background.
     96      */
     97     public static Job startLint(List<? extends IResource> resources,
     98             IDocument doc, boolean fatalOnly, boolean show) {
     99         if (resources != null && !resources.isEmpty()) {
    100             resources = addLibraries(resources);
    101 
    102             cancelCurrentJobs(false);
    103 
    104             CheckFileJob job = new CheckFileJob(resources, doc, fatalOnly);
    105             job.schedule();
    106 
    107             if (show) {
    108                 // Show lint view where the results are listed
    109                 LintViewPart.show(resources);
    110             }
    111             return job;
    112         }
    113 
    114         return null;
    115     }
    116 
    117     /**
    118      * Run Lint for an Export APK action. If it succeeds (no fatal errors)
    119      * returns true, and if it fails it will display an error message and return
    120      * false.
    121      *
    122      * @param shell the parent shell to show error messages in
    123      * @param project the project to run lint on
    124      * @return true if the lint run succeeded with no fatal errors
    125      */
    126     public static boolean runLintOnExport(Shell shell, IProject project) {
    127         if (AdtPrefs.getPrefs().isLintOnExport()) {
    128             boolean fatal = EclipseLintRunner.runLint(Collections.singletonList(project), null,
    129                     true /*fatalOnly*/);
    130             if (fatal) {
    131                 MessageDialog.openWarning(shell,
    132                         "Export Aborted",
    133                         "Export aborted because fatal lint errors were found. These " +
    134                         "are listed in the Lint View. Either fix these before " +
    135                         "running Export again, or turn off \"Run full error check " +
    136                         "when exporting app\" in the Android > Lint Error Checking " +
    137                         "preference page.");
    138                 return false;
    139             }
    140         }
    141 
    142         return true;
    143     }
    144 
    145     /** Returns the current lint jobs, if any (never returns null but array may be empty) */
    146     static Job[] getCurrentJobs() {
    147         IJobManager jobManager = Job.getJobManager();
    148         return jobManager.find(CheckFileJob.FAMILY_RUN_LINT);
    149     }
    150 
    151     /** Cancels the current lint jobs, if any, and optionally waits for them to finish */
    152     static void cancelCurrentJobs(boolean wait) {
    153         // Cancel any current running jobs first
    154         Job[] currentJobs = getCurrentJobs();
    155         for (Job job : currentJobs) {
    156             job.cancel();
    157         }
    158 
    159         if (wait) {
    160             for (Job job : currentJobs) {
    161                 try {
    162                     job.join();
    163                 } catch (InterruptedException e) {
    164                     AdtPlugin.log(e, null);
    165                 }
    166             }
    167         }
    168     }
    169 
    170     /** If the resource list contains projects, add in any library projects as well */
    171     private static List<? extends IResource> addLibraries(List<? extends IResource> resources) {
    172         if (resources != null && !resources.isEmpty()) {
    173             boolean haveProjects = false;
    174             for (IResource resource : resources) {
    175                 if (resource instanceof IProject) {
    176                     haveProjects = true;
    177                     break;
    178                 }
    179             }
    180 
    181             if (haveProjects) {
    182                 List<IResource> result = new ArrayList<IResource>();
    183                 Map<IProject, IProject> allProjects = new IdentityHashMap<IProject, IProject>();
    184                 List<IProject> projects = new ArrayList<IProject>();
    185                 for (IResource resource : resources) {
    186                     if (resource instanceof IProject) {
    187                         IProject project = (IProject) resource;
    188                         allProjects.put(project, project);
    189                         projects.add(project);
    190                     } else {
    191                         result.add(resource);
    192                     }
    193                 }
    194                 for (IProject project : projects) {
    195                     ProjectState state = Sdk.getProjectState(project);
    196                     if (state != null) {
    197                         for (IProject library : state.getFullLibraryProjects()) {
    198                             allProjects.put(library, library);
    199                         }
    200                     }
    201                 }
    202                 for (IProject project : allProjects.keySet()) {
    203                     result.add(project);
    204                 }
    205 
    206                 return result;
    207             }
    208         }
    209 
    210         return resources;
    211     }
    212 
    213     private static final class CheckFileJob extends Job {
    214         /** Job family */
    215         private static final Object FAMILY_RUN_LINT = new Object();
    216         private final List<? extends IResource> mResources;
    217         private final IDocument mDocument;
    218         private LintDriver mLint;
    219         private boolean mFatal;
    220         private boolean mFatalOnly;
    221 
    222         private CheckFileJob(List<? extends IResource> resources, IDocument doc,
    223                 boolean fatalOnly) {
    224             super("Running Android Lint");
    225             mResources = resources;
    226             mDocument = doc;
    227             mFatalOnly = fatalOnly;
    228         }
    229 
    230         @Override
    231         public boolean belongsTo(Object family) {
    232             return family == FAMILY_RUN_LINT;
    233         }
    234 
    235         @Override
    236         protected void canceling() {
    237             super.canceling();
    238             if (mLint != null) {
    239                 mLint.cancel();
    240             }
    241         }
    242 
    243         @Override
    244         protected IStatus run(IProgressMonitor monitor) {
    245             try {
    246                 monitor.beginTask("Looking for errors", IProgressMonitor.UNKNOWN);
    247                 IssueRegistry registry = EclipseLintClient.getRegistry();
    248                 EnumSet<Scope> scope = Scope.ALL;
    249                 List<File> files = new ArrayList<File>(mResources.size());
    250                 for (IResource resource : mResources) {
    251                     File file = AdtUtils.getAbsolutePath(resource).toFile();
    252                     files.add(file);
    253 
    254                     if (resource instanceof IProject) {
    255                         scope = Scope.ALL;
    256                     } else if (resource instanceof IFile
    257                             && AdtUtils.endsWithIgnoreCase(resource.getName(), DOT_XML)) {
    258                         if (resource.getName().equals(SdkConstants.FN_ANDROID_MANIFEST_XML)) {
    259                             scope = EnumSet.of(Scope.MANIFEST);
    260                         } else {
    261                             scope = Scope.RESOURCE_FILE_SCOPE;
    262                         }
    263                     } else {
    264                         return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR,
    265                                 "Only XML files are supported for single file lint", null); //$NON-NLS-1$
    266                     }
    267                 }
    268                 if (Scope.checkSingleFile(scope)) {
    269                     // Delete specific markers
    270                     for (IResource resource : mResources) {
    271                         IMarker[] markers = EclipseLintClient.getMarkers(resource);
    272                         for (IMarker marker : markers) {
    273                             String id = marker.getAttribute(MARKER_CHECKID_PROPERTY, ""); //$NON-NLS-1$
    274                             Issue issue = registry.getIssue(id);
    275                             if (issue == null || issue.getScope().equals(scope)) {
    276                                 marker.delete();
    277                             }
    278                         }
    279                     }
    280                 } else {
    281                     EclipseLintClient.clearMarkers(mResources);
    282                 }
    283 
    284                 EclipseLintClient client = new EclipseLintClient(registry, mResources,
    285                             mDocument, mFatalOnly);
    286                 mLint = new LintDriver(registry, client);
    287                 mLint.analyze(files, scope);
    288                 mFatal = client.hasFatalErrors();
    289                 return Status.OK_STATUS;
    290             } catch (Exception e) {
    291                 return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, Status.ERROR,
    292                                   "Failed", e); //$NON-NLS-1$
    293             } finally {
    294                 if (monitor != null) {
    295                     monitor.done();
    296                 }
    297             }
    298         }
    299 
    300         /**
    301          * Returns true if a fatal error was encountered
    302          */
    303         boolean isFatal() {
    304             return mFatal;
    305         }
    306     }
    307 }
    308