// ===================================================================== // // Copyright (C) 2012 - 2016, Philip Graf // // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // which accompanies this distribution, and is available at // http://www.eclipse.org/legal/epl-v10.html // // ===================================================================== package ch.acanda.eclipse.pmd.java.resolution; import java.lang.reflect.ParameterizedType; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.core.filebuffers.FileBuffers; import org.eclipse.core.filebuffers.ITextFileBuffer; import org.eclipse.core.filebuffers.ITextFileBufferManager; import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.IAnnotationModel; import org.eclipse.swt.graphics.Image; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.texteditor.MarkerAnnotation; import org.eclipse.ui.views.markers.WorkbenchMarkerResolution; import com.google.common.base.Optional; import ch.acanda.eclipse.pmd.marker.PMDMarker; import ch.acanda.eclipse.pmd.ui.util.PMDPluginImages; /** * Base class for a Java quick fix. * * @author Philip Graf * * @param <T> The type of AST node that will be passed to {@link #apply(ASTNode)}. */ public abstract class JavaQuickFix<T extends ASTNode> extends WorkbenchMarkerResolution { private static final IMarker[] NO_OTHER_MARKERS = new IMarker[0]; protected final PMDMarker marker; protected JavaQuickFix(final PMDMarker marker) { this.marker = marker; } @Override public final Image getImage() { return PMDPluginImages.get(getImageDescriptor()); } /** * Returns the image descriptor for the image to be displayed in the list of resolutions. * * @return The image descriptor for the image to be shown. Must not return <code>null</code>. */ abstract protected ImageDescriptor getImageDescriptor(); /** * Returns other markers with the same rule id as the marker of this quick fix. This allows to fix multiple PMD * problems of the same type all at once. */ @Override @SuppressWarnings("PMD.UseVarargs") public IMarker[] findOtherMarkers(final IMarker[] markers) { final IMarker[] result; if (markers.length > 1) { final List<IMarker> otherMarkers = new ArrayList<>(markers.length); for (final IMarker other : markers) { if (marker.isOtherWithSameRuleId(other)) { otherMarkers.add(other); } } if (otherMarkers.isEmpty()) { result = NO_OTHER_MARKERS; } else { result = otherMarkers.toArray(new IMarker[otherMarkers.size()]); } } else { result = NO_OTHER_MARKERS; } return result; } @Override public void run(final IMarker[] markers, final IProgressMonitor monitor) { final Map<IFile, List<IMarker>> map = createMarkerMap(markers); monitor.beginTask(getLabel(), (map.keySet().size() * 2 + markers.length) * 100); try { for (final Entry<IFile, List<IMarker>> entry : map.entrySet()) { fixMarkersInFile(entry.getKey(), entry.getValue(), monitor); } } finally { monitor.done(); } } /** * @return A map grouping the markers by their file. */ private Map<IFile, List<IMarker>> createMarkerMap(final IMarker[] markers) { final Map<IFile, List<IMarker>> markerMap = new HashMap<>(); for (final IMarker marker : markers) { final IResource resource = marker.getResource(); if (resource instanceof IFile && resource.isAccessible()) { final IFile key = (IFile) resource; List<IMarker> value = markerMap.get(key); if (value == null) { value = new ArrayList<IMarker>(); markerMap.put(key, value); } value.add(marker); } } return markerMap; } @Override public void run(final IMarker marker) { final IResource resource = marker.getResource(); if (resource instanceof IFile && resource.isAccessible()) { fixMarkersInFile((IFile) resource, Arrays.asList(marker), new NullProgressMonitor()); } } /** * Fixes all provided markers in a file. * * @param markers The markers to fix. There is at least one marker in this collection and all markers can be fixed * by this quick fix. */ protected void fixMarkersInFile(final IFile file, final List<IMarker> markers, final IProgressMonitor monitor) { monitor.subTask(file.getFullPath().toOSString()); final Optional<ICompilationUnit> optionalCompilationUnit = getCompilationUnit(file); if (!optionalCompilationUnit.isPresent()) { return; } final ICompilationUnit compilationUnit = optionalCompilationUnit.get(); ITextFileBufferManager bufferManager = null; final IPath path = compilationUnit.getPath(); try { bufferManager = FileBuffers.getTextFileBufferManager(); bufferManager.connect(path, LocationKind.IFILE, null); final ITextFileBuffer textFileBuffer = bufferManager.getTextFileBuffer(path, LocationKind.IFILE); final IDocument document = textFileBuffer.getDocument(); final IAnnotationModel annotationModel = textFileBuffer.getAnnotationModel(); final ASTParser astParser = ASTParser.newParser(AST.JLS4); astParser.setKind(ASTParser.K_COMPILATION_UNIT); astParser.setResolveBindings(needsTypeResolution()); astParser.setSource(compilationUnit); final SubProgressMonitor parserMonitor = new SubProgressMonitor(monitor, 100); final CompilationUnit ast = (CompilationUnit) astParser.createAST(parserMonitor); parserMonitor.done(); startFixingMarkers(ast); final Map<?, ?> options = compilationUnit.getJavaProject().getOptions(true); for (final IMarker marker : markers) { try { final MarkerAnnotation annotation = getMarkerAnnotation(annotationModel, marker); // if the annotation is null it means that is was deleted by a previous quick fix if (annotation != null) { final Optional<T> node = getNodeFinder(annotationModel.getPosition(annotation)).findNode(ast); if (node.isPresent()) { final boolean isSuccessful = fixMarker(node.get(), document, options); if (isSuccessful) { marker.delete(); } } } } finally { monitor.worked(100); } } finishFixingMarkers(ast, document, options); // commit changes to underlying file if it is not opened in an editor if (!isEditorOpen(file)) { final SubProgressMonitor commitMonitor = new SubProgressMonitor(monitor, 100); textFileBuffer.commit(commitMonitor, false); commitMonitor.done(); } else { monitor.worked(100); } } catch (CoreException | MalformedTreeException | BadLocationException e) { // TODO: log error // PMDPlugin.getDefault().error("Error processing quickfix", e); } finally { if (bufferManager != null) { try { bufferManager.disconnect(path, LocationKind.IFILE, null); } catch (final CoreException e) { // TODO: log error // PMDPlugin.getDefault().error("Error processing quickfix", e); } } } } /** * Returns {@code true} if the quick fix needs resolved types. Type resolution comes at a considerable cost in both * time and space, however, and should not be requested frivolously. The additional space is not reclaimed until the * AST, all its nodes, and all its bindings become garbage. So it is very important to not retain any of these * objects longer than absolutely necessary. * * @see ASTParser#setResolveBindings(boolean) */ protected boolean needsTypeResolution() { return false; } /** * Prepares the quick fix for fixing the markers. This method is guaranteed to be invoked before * {@link #fixMarker(ASTNode, IDocument, Map)} and {@link #finishFixingMarkers(CompilationUnit, IDocument, Map). */ protected abstract void startFixingMarkers(final CompilationUnit ast); /** * Fixes a single marker. The marker is already resolve to its corresponding node in the AST. This method is * guaranteed to be invoked before {@link #finishFixingMarkers(CompilationUnit, IDocument, Map). * * @param node The marker's corresponding node in the AST. * @param document The document representing the Java file. * @param options The project's Java options. * @return {@code true} iff the quick fix was applied successfully, i.e. the PMD problem was resolved. If * {@code false} is returned then the AST must not have been modified in any way. * @throws CoreException Thrown when the AST has already been modified but the fix could not have been successfully * applied. Throwing this exception will abort all quick fixes for this file. Any already successfully * applied quick fixes will not be committed. */ protected abstract boolean fixMarker(final T node, final IDocument document, final Map<?, ?> options) throws CoreException; /** * Finishes fixing the markers. After this method the document should have its final form before being committed. * * @param ast The document's AST. * @param document The document representing the Java file. * @param options The project's Java options. * @throws BadLocationException Thrown when the fixing cannot be finished properly. The already applied quick fixes * will not be committed. */ protected abstract void finishFixingMarkers(final CompilationUnit ast, final IDocument document, final Map<?, ?> options) throws BadLocationException; /** * @param position The position of the marker. * @return The node finder that will be used to search for the node which will be passed to the quick fix. */ protected abstract NodeFinder<CompilationUnit, T> getNodeFinder(final Position position); /** * Returns the type of the AST node that will be used to find the node that will be used as an argument when * invoking {@link #apply(ASTNode)}. This method takes the type from the type parameter of this class. * * @return The type of the node that will be used when invoking {@link #apply(ASTNode)}. */ @SuppressWarnings("unchecked") protected Class<T> getNodeType() { // This works only if 'this' is a direct subclass of ASTQuickFix. final ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass(); return (Class<T>) genericSuperclass.getActualTypeArguments()[0]; } // /** // * Applies the quick fix to the provided node. The marker's range lies within the node's range and the node's type // * is the same as the one returned by {@link #getNodeType()}. // * // * @return {@code true} iff the quick fix was applied successfully, i.e. the PMD problem was resolved. // */ // protected abstract boolean apply(final T node); private Optional<ICompilationUnit> getCompilationUnit(final IFile file) { final IJavaElement element = JavaCore.create(file); return element instanceof ICompilationUnit ? Optional.of((ICompilationUnit) element) : Optional.<ICompilationUnit>absent(); } private MarkerAnnotation getMarkerAnnotation(final IAnnotationModel annotationModel, final IMarker marker) { @SuppressWarnings("unchecked") final Iterator<Annotation> annotations = annotationModel.getAnnotationIterator(); while (annotations.hasNext()) { final Annotation annotation = annotations.next(); if (annotation instanceof MarkerAnnotation) { final IMarker annotationMarker = ((MarkerAnnotation) annotation).getMarker(); if (annotationMarker.equals(marker)) { return (MarkerAnnotation) annotation; } } } return null; } /** * @return {@code true} iff an editor for the provided file is open. The editor must not necessarily be visible or * even belong to the active window or perspective. */ private boolean isEditorOpen(final IFile file) { for (final IWorkbenchWindow window : PlatformUI.getWorkbench().getWorkbenchWindows()) { for (final IWorkbenchPage page : window.getPages()) { for (final IEditorReference reference : page.getEditorReferences()) { try { final IEditorInput input = reference.getEditorInput(); if (input instanceof IFileEditorInput && file.equals(((IFileEditorInput) input).getFile())) { return true; } } catch (final PartInitException e) { // cannot get editor input -> ignore } } } } return false; } }