blob: da100850ad938dc9307ade932eccd7a477ef6a04 [file] [log] [blame]
/*
* 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.TextFormat;
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));
proposals.addAll(AddSuppressAttribute.createFixes(editor, marker, id));
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;
List<AddSuppressAttribute> fixes = AddSuppressAttribute.createFixes(editor,
marker, id);
if (fixes.size() > 0) {
IStructuredDocument document = editor.getStructuredDocument();
fixes.get(0).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');
String explanation = issue.getExplanation(TextFormat.TEXT);
if (explanation != null && !explanation.isEmpty()) {
sb.append('\n');
sb.append(explanation);
} else {
sb.append(issue.getBriefDescription(TextFormat.TEXT));
}
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 String.format("Explain Issue (%1$s)", mId);
}
// ---- 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).getExplanation(
TextFormat.HTML);
}
@Override
public Image getImage() {
ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
return sharedImages.getImage(ISharedImages.IMG_OBJS_INFO_TSK);
}
@Override
public IContextInformation getContextInformation() {
return null;
}
}
}