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_JAVA; 19 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; 20 21 import com.android.ide.eclipse.adt.AdtConstants; 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.ide.eclipse.adt.AdtUtils; 24 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 25 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 26 import com.android.tools.lint.client.api.Configuration; 27 import com.android.tools.lint.client.api.DefaultConfiguration; 28 import com.android.tools.lint.client.api.IssueRegistry; 29 import com.android.tools.lint.detector.api.Issue; 30 import com.android.tools.lint.detector.api.Project; 31 import com.android.tools.lint.detector.api.Severity; 32 33 import org.eclipse.core.resources.IFile; 34 import org.eclipse.core.resources.IMarker; 35 import org.eclipse.core.resources.IProject; 36 import org.eclipse.core.resources.IResource; 37 import org.eclipse.core.runtime.CoreException; 38 import org.eclipse.jface.dialogs.MessageDialog; 39 import org.eclipse.jface.text.IDocument; 40 import org.eclipse.jface.text.IRegion; 41 import org.eclipse.jface.text.Region; 42 import org.eclipse.jface.text.contentassist.ICompletionProposal; 43 import org.eclipse.jface.text.contentassist.IContextInformation; 44 import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext; 45 import org.eclipse.jface.text.quickassist.IQuickAssistProcessor; 46 import org.eclipse.jface.text.source.Annotation; 47 import org.eclipse.jface.text.source.ISourceViewer; 48 import org.eclipse.swt.graphics.Image; 49 import org.eclipse.swt.graphics.Point; 50 import org.eclipse.ui.IEditorInput; 51 import org.eclipse.ui.IEditorPart; 52 import org.eclipse.ui.IMarkerResolution; 53 import org.eclipse.ui.IMarkerResolution2; 54 import org.eclipse.ui.IMarkerResolutionGenerator2; 55 import org.eclipse.ui.ISharedImages; 56 import org.eclipse.ui.PartInitException; 57 import org.eclipse.ui.PlatformUI; 58 import org.eclipse.ui.part.FileEditorInput; 59 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 60 61 import java.io.File; 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.List; 65 66 /** 67 * A quickfix and marker resolution for disabling lint checks, and any 68 * IDE specific implementations for fixing the warnings. 69 * <p> 70 * I would really like for this quickfix to show up as a light bulb on top of the error 71 * icon in the editor, and I've spent a whole day trying to make it work. I did not 72 * succeed, but here are the steps I tried in case I want to pick up the work again 73 * later: 74 * <ul> 75 * <li> 76 * The WST has some support for quick fixes, and I came across some forum posts 77 * referencing the ability to show light bulbs. However, it turns out that the 78 * quickfix support for annotations in WST is hardcoded to source validation 79 * errors *only*. 80 * <li> 81 * I tried defining my own editor annotations, and customizing the icon directly 82 * by either setting an icon or using the image provider. This works fine 83 * if I make my marker be a new independent marker type. However, whenever I 84 * switch the marker type back to extend the "Problem" type, then the icon reverts 85 * back to the standard error icon and it ignores my custom settings. 86 * And if I switch away from the Problems marker type, then the errors no longer 87 * show up in the Problems view. (I also tried extending the JDT marker but that 88 * still didn't work.) 89 * <li> 90 * It looks like only JDT handles quickfix icons. It has a bunch of custom code 91 * to handle this, along with its own Annotation subclass used by the editor. 92 * I tried duplicating some of this by subclassing StructuredTextEditor, but 93 * it was evident that I'd have to pull in a *huge* amount of duplicated code to 94 * make this work, which seems risky given that all this is internal code that 95 * can change from one Eclipse version to the next. 96 * </ul> 97 * It looks like our best bet would be to reconsider whether these should show up 98 * in the Problems view; perhaps we should use a custom view for these. That would also 99 * make marker management more obvious. 100 */ 101 public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssistProcessor { 102 /** Constructs a new {@link LintFixGenerator} */ LintFixGenerator()103 public LintFixGenerator() { 104 } 105 106 // ---- Implements IMarkerResolutionGenerator2 ---- 107 108 @Override hasResolutions(IMarker marker)109 public boolean hasResolutions(IMarker marker) { 110 try { 111 assert marker.getType().equals(AdtConstants.MARKER_LINT); 112 } catch (CoreException e) { 113 } 114 115 return true; 116 } 117 118 @Override getResolutions(IMarker marker)119 public IMarkerResolution[] getResolutions(IMarker marker) { 120 String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, 121 ""); //$NON-NLS-1$ 122 IResource resource = marker.getResource(); 123 124 List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); 125 126 if (resource.getName().endsWith(DOT_JAVA)) { 127 AddSuppressAnnotation.createFixes(marker, id, resolutions); 128 } 129 130 resolutions.add(new MoreInfoProposal(id, marker.getAttribute(IMarker.MESSAGE, null))); 131 resolutions.add(new SuppressProposal(resource, id, false)); 132 resolutions.add(new SuppressProposal(resource.getProject(), id, true /* all */)); 133 resolutions.add(new SuppressProposal(resource, id, true /* all */)); 134 resolutions.add(new ClearMarkersProposal(resource, true /* all */)); 135 136 if (resolutions.size() > 0) { 137 return resolutions.toArray(new IMarkerResolution[resolutions.size()]); 138 } 139 140 return null; 141 } 142 143 // ---- Implements IQuickAssistProcessor ---- 144 145 @Override getErrorMessage()146 public String getErrorMessage() { 147 return "Disable Lint Error"; 148 } 149 150 @Override canFix(Annotation annotation)151 public boolean canFix(Annotation annotation) { 152 return true; 153 } 154 155 @Override canAssist(IQuickAssistInvocationContext invocationContext)156 public boolean canAssist(IQuickAssistInvocationContext invocationContext) { 157 return true; 158 } 159 160 @Override computeQuickAssistProposals( IQuickAssistInvocationContext invocationContext)161 public ICompletionProposal[] computeQuickAssistProposals( 162 IQuickAssistInvocationContext invocationContext) { 163 ISourceViewer sourceViewer = invocationContext.getSourceViewer(); 164 AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer); 165 if (editor != null) { 166 IFile file = editor.getInputFile(); 167 if (file == null) { 168 return null; 169 } 170 IDocument document = sourceViewer.getDocument(); 171 List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_LINT, 172 file, document, invocationContext.getOffset()); 173 List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); 174 if (markers.size() > 0) { 175 for (IMarker marker : markers) { 176 String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, 177 ""); //$NON-NLS-1$ 178 179 // TODO: Allow for more than one fix? 180 ICompletionProposal fix = LintFix.getFix(id, marker); 181 if (fix != null) { 182 proposals.add(fix); 183 } 184 185 String message = marker.getAttribute(IMarker.MESSAGE, null); 186 proposals.add(new MoreInfoProposal(id, message)); 187 188 fix = AddSuppressAttribute.createFix(editor, marker, id); 189 if (fix != null) { 190 proposals.add(fix); 191 } 192 193 proposals.add(new SuppressProposal(file, id, false)); 194 proposals.add(new SuppressProposal(file.getProject(), id, true /* all */)); 195 proposals.add(new SuppressProposal(file, id, true /* all */)); 196 197 proposals.add(new ClearMarkersProposal(file, true /* all */)); 198 } 199 } 200 if (proposals.size() > 0) { 201 return proposals.toArray(new ICompletionProposal[proposals.size()]); 202 } 203 } 204 205 return null; 206 } 207 208 /** 209 * Suppress the given detector, and rerun the checks on the file 210 * 211 * @param id the id of the detector to be suppressed, or null 212 * @param updateMarkers if true, update all markers 213 * @param resource the resource associated with the markers 214 * @param thisFileOnly if true, only suppress this issue in this file 215 */ suppressDetector(String id, boolean updateMarkers, IResource resource, boolean thisFileOnly)216 public static void suppressDetector(String id, boolean updateMarkers, IResource resource, 217 boolean thisFileOnly) { 218 IssueRegistry registry = EclipseLintClient.getRegistry(); 219 Issue issue = registry.getIssue(id); 220 if (issue != null) { 221 EclipseLintClient mClient = new EclipseLintClient(registry, 222 Collections.singletonList(resource), null, false); 223 Project project = null; 224 IProject eclipseProject = resource.getProject(); 225 if (eclipseProject != null) { 226 File dir = AdtUtils.getAbsolutePath(eclipseProject).toFile(); 227 project = mClient.getProject(dir, dir); 228 } 229 Configuration configuration = mClient.getConfiguration(project); 230 if (thisFileOnly && configuration instanceof DefaultConfiguration) { 231 File file = AdtUtils.getAbsolutePath(resource).toFile(); 232 ((DefaultConfiguration) configuration).ignore(issue, file); 233 } else { 234 configuration.setSeverity(issue, Severity.IGNORE); 235 } 236 } 237 238 if (updateMarkers) { 239 EclipseLintClient.removeMarkers(resource, id); 240 } 241 } 242 243 /** 244 * Adds a suppress lint annotation or attribute depending on whether the 245 * error is in a Java or XML file. 246 * 247 * @param marker the marker pointing to the error to be suppressed 248 */ 249 @SuppressWarnings("restriction") // XML model addSuppressAnnotation(IMarker marker)250 public static void addSuppressAnnotation(IMarker marker) { 251 String id = EclipseLintClient.getId(marker); 252 if (id != null) { 253 IResource resource = marker.getResource(); 254 if (!(resource instanceof IFile)) { 255 return; 256 } 257 IFile file = (IFile) resource; 258 boolean isJava = file.getName().endsWith(DOT_JAVA); 259 boolean isXml = AdtUtils.endsWith(file.getName(), DOT_XML); 260 if (!isJava && !isXml) { 261 return; 262 } 263 264 try { 265 IEditorPart activeEditor = AdtUtils.getActiveEditor(); 266 IEditorPart part = null; 267 if (activeEditor != null) { 268 IEditorInput input = activeEditor.getEditorInput(); 269 if (input instanceof FileEditorInput 270 && ((FileEditorInput)input).getFile().equals(file)) { 271 part = activeEditor; 272 } 273 } 274 if (part == null) { 275 IRegion region = null; 276 int start = marker.getAttribute(IMarker.CHAR_START, -1); 277 int end = marker.getAttribute(IMarker.CHAR_END, -1); 278 if (start != -1 && end != -1) { 279 region = new Region(start, end - start); 280 } 281 part = AdtPlugin.openFile(file, region, true /* showEditor */); 282 } 283 284 if (isJava) { 285 List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); 286 AddSuppressAnnotation.createFixes(marker, id, resolutions); 287 if (resolutions.size() > 0) { 288 resolutions.get(0).run(marker); 289 } 290 } else { 291 assert isXml; 292 if (part instanceof AndroidXmlEditor) { 293 AndroidXmlEditor editor = (AndroidXmlEditor) part; 294 AddSuppressAttribute fix = AddSuppressAttribute.createFix(editor, 295 marker, id); 296 if (fix != null) { 297 IStructuredDocument document = editor.getStructuredDocument(); 298 fix.apply(document); 299 } 300 } 301 } 302 } catch (PartInitException pie) { 303 AdtPlugin.log(pie, null); 304 } 305 } 306 } 307 308 private static class SuppressProposal implements ICompletionProposal, IMarkerResolution2 { 309 private final String mId; 310 private final boolean mGlobal; 311 private final IResource mResource; 312 SuppressProposal(IResource resource, String check, boolean global)313 private SuppressProposal(IResource resource, String check, boolean global) { 314 mResource = resource; 315 mId = check; 316 mGlobal = global; 317 } 318 perform()319 private void perform() { 320 suppressDetector(mId, true, mResource, !mGlobal); 321 } 322 323 @Override getDisplayString()324 public String getDisplayString() { 325 if (mResource instanceof IProject) { 326 return "Disable Check in This Project"; 327 } else if (mGlobal) { 328 return "Disable Check"; 329 } else { 330 return "Disable Check in This File Only"; 331 } 332 } 333 334 // ---- Implements MarkerResolution2 ---- 335 336 @Override getLabel()337 public String getLabel() { 338 return getDisplayString(); 339 } 340 341 @Override run(IMarker marker)342 public void run(IMarker marker) { 343 perform(); 344 } 345 346 @Override getDescription()347 public String getDescription() { 348 return getAdditionalProposalInfo(); 349 } 350 351 // ---- Implements ICompletionProposal ---- 352 353 @Override apply(IDocument document)354 public void apply(IDocument document) { 355 perform(); 356 } 357 358 @Override getSelection(IDocument document)359 public Point getSelection(IDocument document) { 360 return null; 361 } 362 363 @Override getAdditionalProposalInfo()364 public String getAdditionalProposalInfo() { 365 StringBuilder sb = new StringBuilder(200); 366 if (mResource instanceof IProject) { 367 sb.append("Suppresses this type of lint warning in the current project only."); 368 } else if (mGlobal) { 369 sb.append("Suppresses this type of lint warning in all files."); 370 } else { 371 sb.append("Suppresses this type of lint warning in the current file only."); 372 } 373 sb.append("<br><br>"); //$NON-NLS-1$ 374 sb.append("You can re-enable checks from the \"Android > Lint Error Checking\" preference page."); 375 376 return sb.toString(); 377 } 378 379 @Override getImage()380 public Image getImage() { 381 ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); 382 return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK); 383 } 384 385 @Override getContextInformation()386 public IContextInformation getContextInformation() { 387 return null; 388 } 389 } 390 391 private static class ClearMarkersProposal implements ICompletionProposal, IMarkerResolution2 { 392 private final boolean mGlobal; 393 private final IResource mResource; 394 ClearMarkersProposal(IResource resource, boolean global)395 public ClearMarkersProposal(IResource resource, boolean global) { 396 mResource = resource; 397 mGlobal = global; 398 } 399 perform()400 private void perform() { 401 IResource resource = mGlobal ? mResource.getProject() : mResource; 402 EclipseLintClient.clearMarkers(resource); 403 } 404 405 @Override getDisplayString()406 public String getDisplayString() { 407 return mGlobal ? "Clear All Lint Markers" : "Clear Markers in This File Only"; 408 } 409 410 // ---- Implements MarkerResolution2 ---- 411 412 @Override getLabel()413 public String getLabel() { 414 return getDisplayString(); 415 } 416 417 @Override run(IMarker marker)418 public void run(IMarker marker) { 419 perform(); 420 } 421 422 @Override getDescription()423 public String getDescription() { 424 return getAdditionalProposalInfo(); 425 } 426 427 // ---- Implements ICompletionProposal ---- 428 429 @Override apply(IDocument document)430 public void apply(IDocument document) { 431 perform(); 432 } 433 434 @Override getSelection(IDocument document)435 public Point getSelection(IDocument document) { 436 return null; 437 } 438 439 @Override getAdditionalProposalInfo()440 public String getAdditionalProposalInfo() { 441 StringBuilder sb = new StringBuilder(200); 442 if (mGlobal) { 443 sb.append("Clears all lint warning markers from the project."); 444 } else { 445 sb.append("Clears all lint warnings from this file."); 446 } 447 sb.append("<br><br>"); //$NON-NLS-1$ 448 sb.append("This temporarily hides the problem, but does not suppress it. " + 449 "Running Lint again can bring the error back."); 450 if (AdtPrefs.getPrefs().isLintOnSave()) { 451 sb.append(' '); 452 sb.append("This will happen the next time the file is saved since lint-on-save " + 453 "is enabled. You can turn this off in the \"Lint Error Checking\" " + 454 "preference page."); 455 } 456 457 return sb.toString(); 458 } 459 460 @Override getImage()461 public Image getImage() { 462 ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); 463 return sharedImages.getImage(ISharedImages.IMG_ELCL_REMOVE); 464 } 465 466 @Override getContextInformation()467 public IContextInformation getContextInformation() { 468 return null; 469 } 470 } 471 472 private static class MoreInfoProposal implements ICompletionProposal, IMarkerResolution2 { 473 private final String mId; 474 private final String mMessage; 475 MoreInfoProposal(String id, String message)476 public MoreInfoProposal(String id, String message) { 477 mId = id; 478 mMessage = message; 479 } 480 perform()481 private void perform() { 482 Issue issue = EclipseLintClient.getRegistry().getIssue(mId); 483 assert issue != null : mId; 484 485 StringBuilder sb = new StringBuilder(300); 486 sb.append(mMessage); 487 sb.append('\n').append('\n'); 488 sb.append("Issue Explanation:"); 489 sb.append('\n'); 490 if (issue.getExplanation() != null) { 491 sb.append('\n'); 492 sb.append(issue.getExplanation()); 493 } else { 494 sb.append(issue.getDescription()); 495 } 496 497 if (issue.getMoreInfo() != null) { 498 sb.append('\n').append('\n'); 499 sb.append("More Information: "); 500 sb.append(issue.getMoreInfo()); 501 } 502 503 MessageDialog.openInformation(AdtPlugin.getDisplay().getActiveShell(), "More Info", 504 sb.toString()); 505 } 506 507 @Override getDisplayString()508 public String getDisplayString() { 509 return "Explain Issue"; 510 } 511 512 // ---- Implements MarkerResolution2 ---- 513 514 @Override getLabel()515 public String getLabel() { 516 return getDisplayString(); 517 } 518 519 @Override run(IMarker marker)520 public void run(IMarker marker) { 521 perform(); 522 } 523 524 @Override getDescription()525 public String getDescription() { 526 return getAdditionalProposalInfo(); 527 } 528 529 // ---- Implements ICompletionProposal ---- 530 531 @Override apply(IDocument document)532 public void apply(IDocument document) { 533 perform(); 534 } 535 536 @Override getSelection(IDocument document)537 public Point getSelection(IDocument document) { 538 return null; 539 } 540 541 @Override getAdditionalProposalInfo()542 public String getAdditionalProposalInfo() { 543 return "Provides more information about this issue"; 544 } 545 546 @Override getImage()547 public Image getImage() { 548 ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); 549 return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK); 550 } 551 552 @Override getContextInformation()553 public IContextInformation getContextInformation() { 554 return null; 555 } 556 } 557 } 558