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