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