/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.lint; import static com.android.SdkConstants.DOT_JAVA; import static com.android.SdkConstants.DOT_XML; import com.android.ide.eclipse.adt.AdtConstants; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.AdtUtils; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.tools.lint.client.api.Configuration; import com.android.tools.lint.client.api.DefaultConfiguration; import com.android.tools.lint.client.api.IssueRegistry; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.Project; import com.android.tools.lint.detector.api.Severity; import com.android.utils.SdkUtils; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext; import org.eclipse.jface.text.quickassist.IQuickAssistProcessor; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IMarkerResolution; import org.eclipse.ui.IMarkerResolution2; import org.eclipse.ui.IMarkerResolutionGenerator2; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.part.FileEditorInput; import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * A quickfix and marker resolution for disabling lint checks, and any * IDE specific implementations for fixing the warnings. * <p> * I would really like for this quickfix to show up as a light bulb on top of the error * icon in the editor, and I've spent a whole day trying to make it work. I did not * succeed, but here are the steps I tried in case I want to pick up the work again * later: * <ul> * <li> * The WST has some support for quick fixes, and I came across some forum posts * referencing the ability to show light bulbs. However, it turns out that the * quickfix support for annotations in WST is hardcoded to source validation * errors *only*. * <li> * I tried defining my own editor annotations, and customizing the icon directly * by either setting an icon or using the image provider. This works fine * if I make my marker be a new independent marker type. However, whenever I * switch the marker type back to extend the "Problem" type, then the icon reverts * back to the standard error icon and it ignores my custom settings. * And if I switch away from the Problems marker type, then the errors no longer * show up in the Problems view. (I also tried extending the JDT marker but that * still didn't work.) * <li> * It looks like only JDT handles quickfix icons. It has a bunch of custom code * to handle this, along with its own Annotation subclass used by the editor. * I tried duplicating some of this by subclassing StructuredTextEditor, but * it was evident that I'd have to pull in a *huge* amount of duplicated code to * make this work, which seems risky given that all this is internal code that * can change from one Eclipse version to the next. * </ul> * It looks like our best bet would be to reconsider whether these should show up * in the Problems view; perhaps we should use a custom view for these. That would also * make marker management more obvious. */ @SuppressWarnings("restriction") // DOM model public class LintFixGenerator implements IMarkerResolutionGenerator2, IQuickAssistProcessor { /** Constructs a new {@link LintFixGenerator} */ public LintFixGenerator() { } // ---- Implements IMarkerResolutionGenerator2 ---- @Override public boolean hasResolutions(IMarker marker) { try { assert marker.getType().equals(AdtConstants.MARKER_LINT); } catch (CoreException e) { } return true; } @Override public IMarkerResolution[] getResolutions(IMarker marker) { String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, ""); //$NON-NLS-1$ IResource resource = marker.getResource(); List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); if (resource.getName().endsWith(DOT_JAVA)) { AddSuppressAnnotation.createFixes(marker, id, resolutions); } resolutions.add(new MoreInfoProposal(id, marker.getAttribute(IMarker.MESSAGE, null))); resolutions.add(new SuppressProposal(resource, id, false)); resolutions.add(new SuppressProposal(resource.getProject(), id, true /* all */)); resolutions.add(new SuppressProposal(resource, id, true /* all */)); resolutions.add(new ClearMarkersProposal(resource, true /* all */)); if (resolutions.size() > 0) { return resolutions.toArray(new IMarkerResolution[resolutions.size()]); } return null; } // ---- Implements IQuickAssistProcessor ---- @Override public String getErrorMessage() { return "Disable Lint Error"; } @Override public boolean canFix(Annotation annotation) { return true; } @Override public boolean canAssist(IQuickAssistInvocationContext invocationContext) { return true; } @Override public ICompletionProposal[] computeQuickAssistProposals( IQuickAssistInvocationContext invocationContext) { ISourceViewer sourceViewer = invocationContext.getSourceViewer(); AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer); if (editor != null) { IFile file = editor.getInputFile(); if (file == null) { return null; } IDocument document = sourceViewer.getDocument(); List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_LINT, file, document, invocationContext.getOffset()); List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>(); if (markers.size() > 0) { for (IMarker marker : markers) { String id = marker.getAttribute(EclipseLintRunner.MARKER_CHECKID_PROPERTY, ""); //$NON-NLS-1$ // TODO: Allow for more than one fix? List<LintFix> fixes = LintFix.getFixes(id, marker); if (fixes != null) { for (LintFix fix : fixes) { proposals.add(fix); } } String message = marker.getAttribute(IMarker.MESSAGE, null); proposals.add(new MoreInfoProposal(id, message)); ICompletionProposal fix = AddSuppressAttribute.createFix(editor, marker, id); if (fix != null) { proposals.add(fix); } proposals.add(new SuppressProposal(file, id, false)); proposals.add(new SuppressProposal(file.getProject(), id, true /* all */)); proposals.add(new SuppressProposal(file, id, true /* all */)); proposals.add(new ClearMarkersProposal(file, true /* all */)); } } if (proposals.size() > 0) { return proposals.toArray(new ICompletionProposal[proposals.size()]); } } return null; } /** * Suppress the given detector, and rerun the checks on the file * * @param id the id of the detector to be suppressed, or null * @param updateMarkers if true, update all markers * @param resource the resource associated with the markers * @param thisFileOnly if true, only suppress this issue in this file */ public static void suppressDetector(String id, boolean updateMarkers, IResource resource, boolean thisFileOnly) { IssueRegistry registry = EclipseLintClient.getRegistry(); Issue issue = registry.getIssue(id); if (issue != null) { EclipseLintClient mClient = new EclipseLintClient(registry, Collections.singletonList(resource), null, false); Project project = null; IProject eclipseProject = resource.getProject(); if (eclipseProject != null) { File dir = AdtUtils.getAbsolutePath(eclipseProject).toFile(); project = mClient.getProject(dir, dir); } Configuration configuration = mClient.getConfigurationFor(project); if (thisFileOnly && configuration instanceof DefaultConfiguration) { File file = AdtUtils.getAbsolutePath(resource).toFile(); ((DefaultConfiguration) configuration).ignore(issue, file); } else { configuration.setSeverity(issue, Severity.IGNORE); } } if (updateMarkers) { EclipseLintClient.removeMarkers(resource, id); } } /** * Adds a suppress lint annotation or attribute depending on whether the * error is in a Java or XML file. * * @param marker the marker pointing to the error to be suppressed */ public static void addSuppressAnnotation(IMarker marker) { String id = EclipseLintClient.getId(marker); if (id != null) { IResource resource = marker.getResource(); if (!(resource instanceof IFile)) { return; } IFile file = (IFile) resource; boolean isJava = file.getName().endsWith(DOT_JAVA); boolean isXml = SdkUtils.endsWith(file.getName(), DOT_XML); if (!isJava && !isXml) { return; } try { // See if the current active file is the one containing this marker; // if so we can take some shortcuts IEditorPart activeEditor = AdtUtils.getActiveEditor(); IEditorPart part = null; if (activeEditor != null) { IEditorInput input = activeEditor.getEditorInput(); if (input instanceof FileEditorInput && ((FileEditorInput)input).getFile().equals(file)) { part = activeEditor; } } if (part == null) { IRegion region = null; int start = marker.getAttribute(IMarker.CHAR_START, -1); int end = marker.getAttribute(IMarker.CHAR_END, -1); if (start != -1 && end != -1) { region = new Region(start, end - start); } part = AdtPlugin.openFile(file, region, true /* showEditor */); } if (isJava) { List<IMarkerResolution> resolutions = new ArrayList<IMarkerResolution>(); AddSuppressAnnotation.createFixes(marker, id, resolutions); if (resolutions.size() > 0) { resolutions.get(0).run(marker); } } else { assert isXml; if (part instanceof AndroidXmlEditor) { AndroidXmlEditor editor = (AndroidXmlEditor) part; AddSuppressAttribute fix = AddSuppressAttribute.createFix(editor, marker, id); if (fix != null) { IStructuredDocument document = editor.getStructuredDocument(); fix.apply(document); } } } } catch (PartInitException pie) { AdtPlugin.log(pie, null); } } } private static class SuppressProposal implements ICompletionProposal, IMarkerResolution2 { private final String mId; private final boolean mGlobal; private final IResource mResource; private SuppressProposal(IResource resource, String check, boolean global) { mResource = resource; mId = check; mGlobal = global; } private void perform() { suppressDetector(mId, true, mResource, !mGlobal); } @Override public String getDisplayString() { if (mResource instanceof IProject) { return "Disable Check in This Project"; } else if (mGlobal) { return "Disable Check"; } else { return "Disable Check in This File Only"; } } // ---- Implements MarkerResolution2 ---- @Override public String getLabel() { return getDisplayString(); } @Override public void run(IMarker marker) { perform(); } @Override public String getDescription() { return getAdditionalProposalInfo(); } // ---- Implements ICompletionProposal ---- @Override public void apply(IDocument document) { perform(); } @Override public Point getSelection(IDocument document) { return null; } @Override public String getAdditionalProposalInfo() { StringBuilder sb = new StringBuilder(200); if (mResource instanceof IProject) { sb.append("Suppresses this type of lint warning in the current project only."); } else if (mGlobal) { sb.append("Suppresses this type of lint warning in all files."); } else { sb.append("Suppresses this type of lint warning in the current file only."); } sb.append("<br><br>"); //$NON-NLS-1$ sb.append("You can re-enable checks from the \"Android > Lint Error Checking\" preference page."); return sb.toString(); } @Override public Image getImage() { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK); } @Override public IContextInformation getContextInformation() { return null; } } private static class ClearMarkersProposal implements ICompletionProposal, IMarkerResolution2 { private final boolean mGlobal; private final IResource mResource; public ClearMarkersProposal(IResource resource, boolean global) { mResource = resource; mGlobal = global; } private void perform() { IResource resource = mGlobal ? mResource.getProject() : mResource; EclipseLintClient.clearMarkers(resource); } @Override public String getDisplayString() { return mGlobal ? "Clear All Lint Markers" : "Clear Markers in This File Only"; } // ---- Implements MarkerResolution2 ---- @Override public String getLabel() { return getDisplayString(); } @Override public void run(IMarker marker) { perform(); } @Override public String getDescription() { return getAdditionalProposalInfo(); } // ---- Implements ICompletionProposal ---- @Override public void apply(IDocument document) { perform(); } @Override public Point getSelection(IDocument document) { return null; } @Override public String getAdditionalProposalInfo() { StringBuilder sb = new StringBuilder(200); if (mGlobal) { sb.append("Clears all lint warning markers from the project."); } else { sb.append("Clears all lint warnings from this file."); } sb.append("<br><br>"); //$NON-NLS-1$ sb.append("This temporarily hides the problem, but does not suppress it. " + "Running Lint again can bring the error back."); if (AdtPrefs.getPrefs().isLintOnSave()) { sb.append(' '); sb.append("This will happen the next time the file is saved since lint-on-save " + "is enabled. You can turn this off in the \"Lint Error Checking\" " + "preference page."); } return sb.toString(); } @Override public Image getImage() { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); return sharedImages.getImage(ISharedImages.IMG_ELCL_REMOVE); } @Override public IContextInformation getContextInformation() { return null; } } private static class MoreInfoProposal implements ICompletionProposal, IMarkerResolution2 { private final String mId; private final String mMessage; public MoreInfoProposal(String id, String message) { mId = id; mMessage = message; } private void perform() { Issue issue = EclipseLintClient.getRegistry().getIssue(mId); assert issue != null : mId; StringBuilder sb = new StringBuilder(300); sb.append(mMessage); sb.append('\n').append('\n'); sb.append("Issue Explanation:"); sb.append('\n'); if (issue.getExplanation() != null) { sb.append('\n'); sb.append(issue.getExplanationAsSimpleText()); } else { sb.append(issue.getDescription()); } if (issue.getMoreInfo() != null) { sb.append('\n').append('\n'); sb.append("More Information: "); sb.append(issue.getMoreInfo()); } MessageDialog.openInformation(AdtPlugin.getShell(), "More Info", sb.toString()); } @Override public String getDisplayString() { return "Explain Issue"; } // ---- Implements MarkerResolution2 ---- @Override public String getLabel() { return getDisplayString(); } @Override public void run(IMarker marker) { perform(); } @Override public String getDescription() { return getAdditionalProposalInfo(); } // ---- Implements ICompletionProposal ---- @Override public void apply(IDocument document) { perform(); } @Override public Point getSelection(IDocument document) { return null; } @Override public String getAdditionalProposalInfo() { return "Provides more information about this issue." + "<br><br>" //$NON-NLS-1$ + EclipseLintClient.getRegistry().getIssue(mId).getExplanationAsHtml(); } @Override public Image getImage() { ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages(); return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK); } @Override public IContextInformation getContextInformation() { return null; } } }