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