/* * SonarLint for Eclipse * Copyright (C) 2015-2017 SonarSource SA * sonarlint@sonarsource.com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonarlint.eclipse.ui.internal.views.locations; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.eclipse.core.resources.IMarker; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Tree; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.ISelectionListener; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.IDE; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.texteditor.ITextEditor; import org.sonarlint.eclipse.core.SonarLintLogger; import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin; import org.sonarlint.eclipse.core.internal.event.AnalysisEvent; import org.sonarlint.eclipse.core.internal.event.AnalysisListener; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils.ExtraPosition; import org.sonarlint.eclipse.ui.internal.SonarLintImages; import org.sonarlint.eclipse.ui.internal.SonarLintUiPlugin; import org.sonarlint.eclipse.ui.internal.markers.ShowIssueFlowsMarkerResolver; import org.sonarlint.eclipse.ui.internal.views.AbstractSonarWebView; /** * Display details of a rule in a web browser */ public class IssueLocationsView extends ViewPart implements ISelectionListener, AnalysisListener { public static final String ID = SonarLintUiPlugin.PLUGIN_ID + ".views.IssueLocationsView"; private TreeViewer locationsViewer; private ToggleAnnotationsAction showAnnotationsAction; private static class FlowNode { private final String label; private final ExtraPosition position; public FlowNode(ExtraPosition position) { this(positionLabel(position.getMessage()), position); } public FlowNode(String label, ExtraPosition position) { this.label = label; this.position = position; } public String getLabel() { return label; } public ExtraPosition getPosition() { return position; } } static List<FlowNode> toFlowNodes(List<ExtraPosition> positions) { List<FlowNode> result = new ArrayList<>(); int id = positions.size(); for (ExtraPosition extraPosition : positions) { String childLabel = positions.size() > 1 ? (id + ": " + positionLabel(extraPosition.getMessage())) : positionLabel(extraPosition.getMessage()); result.add(new FlowNode(childLabel, extraPosition)); id--; } return result; } private static String positionLabel(String message) { return message != null ? message : ""; } private static class FlowRootNode { private final String label; private final List<FlowNode> children; public FlowRootNode(String label, List<ExtraPosition> positions) { this.label = label; children = toFlowNodes(positions); } public String getLabel() { return label; } public FlowNode[] getChildren() { return children.toArray(new FlowNode[0]); } } private static class RootNode { private final IMarker rootMarker; private final List<List<ExtraPosition>> flows; public RootNode(IMarker rootMarker, List<ExtraPosition> positions) { this.rootMarker = rootMarker; this.flows = rebuildFlows(positions); } private static List<List<ExtraPosition>> rebuildFlows(List<ExtraPosition> positions) { List<List<ExtraPosition>> result = new ArrayList<>(); List<ExtraPosition> roots = positions.stream().filter(p -> p.getParent() == null).collect(Collectors.toList()); for (ExtraPosition root : roots) { List<ExtraPosition> flow = new ArrayList<>(); Optional<ExtraPosition> current = Optional.of(root); while (current.isPresent()) { ExtraPosition currentValue = current.get(); flow.add(currentValue); current = positions.stream().filter( p -> p.getParent() == currentValue).findFirst(); } result.add(flow); } return result; } public IMarker getMarker() { return rootMarker; } public List<List<ExtraPosition>> getFlows() { return flows; } } private static class LocationsProvider implements ITreeContentProvider { @Override public Object[] getElements(Object inputElement) { IMarker sonarlintMarker = (IMarker) inputElement; try { if (SonarLintCorePlugin.MARKER_CHANGESET_ID.equals(sonarlintMarker.getType())) { return new Object[] {"Information not available from the Report view"}; } } catch (CoreException e) { // Ignore } if (sonarlintMarker.getAttribute(MarkerUtils.SONAR_MARKER_HAS_EXTRA_LOCATION_KEY_ATTR, false)) { ITextEditor openEditor = findOpenEditorFor(sonarlintMarker); if (openEditor == null) { return new Object[] {"Please open the file containing this issue in an editor to see the flows"}; } IDocument document = openEditor.getDocumentProvider().getDocument(openEditor.getEditorInput()); return new Object[] {new RootNode(sonarlintMarker, positions(document, p -> p.getMarkerId() == sonarlintMarker.getId()))}; } else { return new Object[] {"No additional locations associated with this issue"}; } } @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof RootNode) { List<List<ExtraPosition>> flows = ((RootNode) parentElement).getFlows(); if (flows.size() > 1) { AtomicInteger counter = new AtomicInteger(0); return flows.stream().map( f -> f.size() > 1 ? new FlowRootNode("Flow " + counter.incrementAndGet(), f) : new FlowNode(f.get(0))).toArray(); } else if (flows.size() == 1) { return toFlowNodes(flows.get(0)).toArray(); } else { return new Object[0]; } } else if (parentElement instanceof FlowRootNode) { return ((FlowRootNode) parentElement).getChildren(); } else { return new Object[0]; } } private static List<ExtraPosition> positions(IDocument document, Predicate<? super ExtraPosition> filter) { try { return Arrays.asList(document.getPositions(MarkerUtils.SONARLINT_EXTRA_POSITIONS_CATEGORY)) .stream() .map( p -> (ExtraPosition) p) .filter(filter).collect(Collectors.toList()); } catch (BadPositionCategoryException e) { SonarLintLogger.get().debug("No extra positions found, should maybe trigger a new analysis"); return Collections.emptyList(); } } @Override public Object getParent(Object element) { return null; } @Override public boolean hasChildren(Object element) { return getChildren(element).length > 0; } @Override public void dispose() { // Do nothing } @Override public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { // Do nothing } } private static class LocationsTreeLabelProvider extends LabelProvider { @Override public String getText(Object element) { if (element instanceof RootNode) { return ((RootNode) element).getMarker().getAttribute(IMarker.MESSAGE, "No message"); } else if (element instanceof FlowRootNode) { return ((FlowRootNode) element).getLabel(); } else if (element instanceof FlowNode) { return ((FlowNode) element).getLabel(); } else if (element instanceof String) { return (String) element; } throw new IllegalArgumentException("Unknow node type: " + element); } @Override public Image getImage(Object element) { if (element instanceof RootNode) { return SonarLintImages.IMG_ISSUE; } return super.getImage(element); } } public void setInput(@Nullable IMarker sonarlintMarker) { locationsViewer.setInput(sonarlintMarker); if (sonarlintMarker != null && showAnnotationsAction.isChecked()) { ITextEditor editorFound = findOpenEditorFor(sonarlintMarker); if (editorFound != null) { ShowIssueFlowsMarkerResolver.showAnnotations(sonarlintMarker, editorFound); } } } @CheckForNull private static ITextEditor findOpenEditorFor(IMarker sonarlintMarker) { // Find IFile and open Editor // Super defensing programming because we don't really understand what is initialized at startup (SLE-122) IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window == null) { return null; } IWorkbenchPage page = window.getActivePage(); if (page == null) { return null; } for (IEditorReference editor : page.getEditorReferences()) { IEditorInput editorInput; try { editorInput = editor.getEditorInput(); } catch (PartInitException e) { SonarLintLogger.get().error("Unable to restore editor", e); continue; } if (editorInput instanceof IFileEditorInput && ((IFileEditorInput) editorInput).getFile().equals(sonarlintMarker.getResource())) { IEditorPart editorPart = editor.getEditor(false); if (editorPart instanceof ITextEditor) { return (ITextEditor) editorPart; } } } return null; } @Override public void selectionChanged(IWorkbenchPart part, ISelection selection) { IMarker selectedMarker = AbstractSonarWebView.findSelectedSonarIssue(selection); if (selectedMarker != null && !Objects.equals(selectedMarker, locationsViewer.getInput())) { setInput(selectedMarker); } } private void startListeningForSelectionChanges() { getSite().getPage().addPostSelectionListener(this); } private void stopListeningForSelectionChanges() { getSite().getPage().removePostSelectionListener(this); } @Override public void createPartControl(Composite parent) { createToolbar(); Tree tree = new Tree(parent, SWT.MULTI); locationsViewer = new TreeViewer(tree); locationsViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); locationsViewer.setUseHashlookup(true); locationsViewer.setContentProvider(new LocationsProvider()); locationsViewer.setLabelProvider(new LocationsTreeLabelProvider()); locationsViewer.addPostSelectionChangedListener( event -> { ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { Object firstElement = ((IStructuredSelection) selection).getFirstElement(); if (firstElement == null) { return; } onTreeNodeSelected(firstElement); } }); locationsViewer.addDoubleClickListener( event -> { ISelection selection = event.getSelection(); if (selection instanceof IStructuredSelection) { Object firstElement = ((IStructuredSelection) selection).getFirstElement(); if (firstElement == null) { return; } onTreeNodeDoubleClick(firstElement); } }); startListeningForSelectionChanges(); SonarLintCorePlugin.getAnalysisListenerManager().addListener(this); } private void onTreeNodeSelected(Object node) { if (node instanceof FlowNode) { IMarker sonarlintMarker = (IMarker) locationsViewer.getInput(); ExtraPosition pos = ((FlowNode) node).getPosition(); ITextEditor openEditor = findOpenEditorFor(sonarlintMarker); if (openEditor != null) { openEditor.setHighlightRange(pos.offset, pos.length, true); } } } private void onTreeNodeDoubleClick(Object node) { IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); try { if (node instanceof RootNode) { IDE.openEditor(page, ((RootNode) node).getMarker()); } else if (node instanceof FlowNode) { IMarker sonarlintMarker = (IMarker) locationsViewer.getInput(); ExtraPosition pos = ((FlowNode) node).getPosition(); IEditorPart editor = IDE.openEditor(page, sonarlintMarker); if (editor instanceof ITextEditor) { ((ITextEditor) editor).selectAndReveal(pos.offset, pos.length); } } } catch ( PartInitException e) { SonarLintLogger.get().error("Unable to open editor", e); } } private void createToolbar() { IToolBarManager toolbarManager = getViewSite().getActionBars().getToolBarManager(); showAnnotationsAction = new ToggleAnnotationsAction(); toolbarManager.add(showAnnotationsAction); toolbarManager.add(new Separator()); toolbarManager.update(false); } @Override public void setFocus() { // Nothing to do } @Override public void dispose() { stopListeningForSelectionChanges(); SonarLintCorePlugin.getAnalysisListenerManager().removeListener(this); ShowIssueFlowsMarkerResolver.removeAllAnnotations(); super.dispose(); } @Override public void analysisCompleted(AnalysisEvent event) { IMarker marker = (IMarker) locationsViewer.getInput(); Display.getDefault().asyncExec(() -> { if (marker != null && marker.exists()) { setInput(marker); } else { setInput(null); } }); } public class ToggleAnnotationsAction extends Action { /** * Constructs a new action. */ public ToggleAnnotationsAction() { super("Toggle annotations"); setDescription("Show/hide annotations in editor"); setToolTipText("Show/hide annotations in editor"); setImageDescriptor(SonarLintImages.MARK_OCCURENCES_IMG); setChecked(true); } /** * Runs the action. */ @Override public void run() { if (isChecked()) { showAnnotations(); } else { ShowIssueFlowsMarkerResolver.removeAllAnnotations(); } } } public void showAnnotations() { IMarker sonarlintMarker = (IMarker) locationsViewer.getInput(); if (sonarlintMarker != null) { ITextEditor editorFound = findOpenEditorFor(sonarlintMarker); if (editorFound != null) { ShowIssueFlowsMarkerResolver.showAnnotations(sonarlintMarker, editorFound); } } } public void setShowAnnotations(boolean b) { showAnnotationsAction.setChecked(b); } }