diff --git a/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/LocalLeakTest.java b/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/LocalLeakTest.java index d29d497ec..fcb6ad521 100644 --- a/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/LocalLeakTest.java +++ b/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/LocalLeakTest.java @@ -46,7 +46,6 @@ public void shouldComputeLocalLeak() throws Exception { SWTBotView view = new OnTheFlyViewBot(bot).show(); assertThat(view.bot().tree().columns()).containsExactly("Date", "Description", "Resource"); - assertThat(view.bot().tree().getAllItems()).isEmpty(); IProject project = importEclipseProject("java/leak", "leak"); JobHelpers.waitForJobsToComplete(bot); diff --git a/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/SecondaryLocationsTest.java b/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/SecondaryLocationsTest.java index d8974e549..5f7f7fc2a 100644 --- a/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/SecondaryLocationsTest.java +++ b/its/org.sonarlint.eclipse.its/src/org/sonarlint/eclipse/its/SecondaryLocationsTest.java @@ -64,11 +64,11 @@ public void closeActiveEditor() { } @Test - public void shouldShowSingleFlow() throws Exception { + public void shouldShowSingleFlow() { SWTBotEclipseEditor helloEditor = openAndAnalyzeFile("SingleFlow.java"); String issueTitle = "\"NullPointerException\" will be thrown when invoking method \"doAnotherThingWith()\"."; - waitUntilOnTheFlyViewHasItemWithTitle(issueTitle + " [+1 flow]"); + waitUntilOnTheFlyViewHasItemWithTitle(issueTitle + " [+5 locations]"); onTheFly.bot().tree().getAllItems()[0].select(); SWTBotView issueLocationsView = getIssueLocationsView(); @@ -86,7 +86,7 @@ public void shouldShowSingleFlow() throws Exception { } @Test - public void shouldShowHighlightsOnly() throws Exception { + public void shouldShowHighlightsOnly() { openAndAnalyzeFile("HighlightOnly.java"); String issueTitle = "Remove these useless parentheses."; @@ -104,7 +104,7 @@ public void shouldShowHighlightsOnly() throws Exception { } @Test - public void shouldShowMultipleFlows() throws Exception { + public void shouldShowMultipleFlows() { SWTBotEclipseEditor helloEditor = openAndAnalyzeFile("MultiFlows.java"); String issueTitle = "\"NullPointerException\" will be thrown when invoking method \"doAnotherThingWith()\"."; @@ -140,7 +140,7 @@ public void shouldShowMultipleFlows() throws Exception { } @Test - public void shouldShowFlattenedFlows() throws Exception { + public void shouldShowFlattenedFlows() { SWTBotEclipseEditor cognitiveComplexityEditor = openAndAnalyzeFile("CognitiveComplexity.java"); String issueTitle = "Refactor this method to reduce its Cognitive Complexity from 24 to the 15 allowed."; diff --git a/org.sonarlint.eclipse.core.tests/src/test/java/org/sonarlint/eclipse/core/internal/markers/MarkerFlowsTest.java b/org.sonarlint.eclipse.core.tests/src/test/java/org/sonarlint/eclipse/core/internal/markers/MarkerFlowsTest.java new file mode 100644 index 000000000..368195024 --- /dev/null +++ b/org.sonarlint.eclipse.core.tests/src/test/java/org/sonarlint/eclipse/core/internal/markers/MarkerFlowsTest.java @@ -0,0 +1,70 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.core.internal.markers; + +import java.util.Arrays; +import org.junit.Test; +import org.sonarlint.eclipse.tests.common.SonarTestCase; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MarkerFlowsTest extends SonarTestCase { + + @Test + public void display_a_correct_summary_for_secondary_locations_only_flows() { + MarkerFlow flow1 = new MarkerFlow(0); + new MarkerFlowLocation(flow1, "message1"); + MarkerFlow flow2 = new MarkerFlow(0); + new MarkerFlowLocation(flow2, "message2"); + MarkerFlow flow3 = new MarkerFlow(0); + new MarkerFlowLocation(flow3, "message3"); + MarkerFlows markerFlows = new MarkerFlows(Arrays.asList(flow1, flow2, flow3)); + + assertThat(markerFlows.getSummaryDescription()).isEqualTo(" [+3 locations]"); + } + + @Test + public void display_a_correct_summary_for_multiple_flows() { + MarkerFlow flow1 = new MarkerFlow(0); + new MarkerFlowLocation(flow1, "message1"); + new MarkerFlowLocation(flow1, "message11"); + MarkerFlow flow2 = new MarkerFlow(0); + new MarkerFlowLocation(flow2, "message2"); + new MarkerFlowLocation(flow2, "message22"); + MarkerFlow flow3 = new MarkerFlow(0); + new MarkerFlowLocation(flow3, "message3"); + new MarkerFlowLocation(flow3, "message33"); + MarkerFlows markerFlows = new MarkerFlows(Arrays.asList(flow1, flow2, flow3)); + + assertThat(markerFlows.getSummaryDescription()).isEqualTo(" [+3 flows]"); + } + + @Test + public void display_a_correct_summary_for_single_flow() { + MarkerFlow flow1 = new MarkerFlow(0); + new MarkerFlowLocation(flow1, "message1"); + new MarkerFlowLocation(flow1, "message11"); + new MarkerFlowLocation(flow1, "message111"); + MarkerFlows markerFlows = new MarkerFlows(Arrays.asList(flow1)); + + assertThat(markerFlows.getSummaryDescription()).isEqualTo(" [+3 locations]"); + } + +} diff --git a/org.sonarlint.eclipse.core/META-INF/MANIFEST.MF b/org.sonarlint.eclipse.core/META-INF/MANIFEST.MF index 69b0846dd..5de79c9f7 100644 --- a/org.sonarlint.eclipse.core/META-INF/MANIFEST.MF +++ b/org.sonarlint.eclipse.core/META-INF/MANIFEST.MF @@ -12,6 +12,7 @@ Bundle-Localization: OSGI-INF/l10n/bundle Export-Package: org.sonarlint.eclipse.core, org.sonarlint.eclipse.core.analysis, org.sonarlint.eclipse.core.configurator, + org.sonarlint.eclipse.core.listener, org.sonarlint.eclipse.core.internal;x-friends:="org.sonarlint.eclipse.core.tests,org.sonarlint.eclipse.ui", org.sonarlint.eclipse.core.internal.adapter;x-friends:="org.sonarlint.eclipse.ui", org.sonarlint.eclipse.core.internal.engine;x-friends:="org.sonarlint.eclipse.ui,org.sonarlint.eclipse.core.tests", diff --git a/org.sonarlint.eclipse.core/plugin.xml b/org.sonarlint.eclipse.core/plugin.xml index e1df22260..404cab599 100644 --- a/org.sonarlint.eclipse.core/plugin.xml +++ b/org.sonarlint.eclipse.core/plugin.xml @@ -88,6 +88,39 @@ + + + + + + + + + + + + + + + + + + + + getBoundProjects(String projectKey) { public void updateProjectStorage(String projectKey, IProgressMonitor monitor) { doWithEngine(engine -> { engine.updateProject(createEndpointParams(), buildClientWithProxyAndCredentials(), projectKey, - false, new WrappedProgressMonitor(monitor, "Update configuration from server '" + getId() + "' for project '" + projectKey + "'")); + true, new WrappedProgressMonitor(monitor, "Update configuration from server '" + getId() + "' for project '" + projectKey + "'")); getBoundProjects(projectKey).forEach(p -> { ProjectBinding projectBinding = engine.calculatePathPrefixes(projectKey, p.files().stream().map(ISonarLintFile::getProjectRelativePath).collect(toList())); String idePathPrefix = projectBinding.idePathPrefix(); @@ -647,7 +647,7 @@ public void downloadServerIssues(String projectKey, IProgressMonitor monitor) { public List downloadServerIssues(ProjectBinding projectBinding, String filePath, IProgressMonitor monitor) { return withEngine( engine -> engine.downloadServerIssues(createEndpointParams(), buildClientWithProxyAndCredentials(), projectBinding, filePath, - false, new WrappedProgressMonitor(monitor, "Fetch issues"))) + true, new WrappedProgressMonitor(monitor, "Fetch issues"))) .orElse(emptyList()); } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/AnalyzeConnectedProjectJob.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/AnalyzeConnectedProjectJob.java index 8bfdc1189..0a74dbd4f 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/AnalyzeConnectedProjectJob.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/AnalyzeConnectedProjectJob.java @@ -79,10 +79,7 @@ protected void trackIssues(Map docPerFile, Map filesWithAtLeastOneIssue = filesWithAtLeastOneIssue(rawIssuesPerResource); - if (!filesWithAtLeastOneIssue.isEmpty()) { - trackServerIssuesAsync(engineFacade, filesWithAtLeastOneIssue, docPerFile, triggerType); - } + trackServerIssuesAsync(engineFacade, rawIssuesPerResource.keySet(), docPerFile, triggerType); } } @@ -97,13 +94,6 @@ protected Collection trackFileIssues(ISonarLintFile file, List filesWithAtLeastOneIssue(Map> rawIssuesPerResource) { - return rawIssuesPerResource.entrySet().stream() - .filter(e -> !e.getValue().isEmpty()) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - } - private void trackServerIssuesAsync(ConnectedEngineFacade engineFacade, Collection resources, Map docPerFile, TriggerType triggerType) { SonarLintCorePlugin.getInstance().getServerIssueUpdater().updateAsync(engineFacade, getProject(), diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/SonarLintMarkerUpdater.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/SonarLintMarkerUpdater.java index b0373b7e2..19d957cfc 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/SonarLintMarkerUpdater.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/jobs/SonarLintMarkerUpdater.java @@ -35,28 +35,45 @@ import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.sonarlint.eclipse.core.SonarLintLogger; import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin; import org.sonarlint.eclipse.core.internal.TriggerType; +import org.sonarlint.eclipse.core.internal.engine.connected.ConnectedEngineFacade; import org.sonarlint.eclipse.core.internal.markers.MarkerFlow; import org.sonarlint.eclipse.core.internal.markers.MarkerFlowLocation; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.internal.markers.TextRange; import org.sonarlint.eclipse.core.internal.preferences.SonarLintGlobalConfiguration; +import org.sonarlint.eclipse.core.internal.preferences.SonarLintProjectConfiguration; +import org.sonarlint.eclipse.core.internal.preferences.SonarLintProjectConfiguration.EclipseProjectBinding; import org.sonarlint.eclipse.core.internal.resources.ProjectsProviderUtils; +import org.sonarlint.eclipse.core.internal.tracking.ServerIssueTrackable; import org.sonarlint.eclipse.core.internal.tracking.Trackable; +import org.sonarlint.eclipse.core.internal.utils.StringUtils; +import org.sonarlint.eclipse.core.listener.TaintVulnerabilitiesListener; import org.sonarlint.eclipse.core.resource.ISonarLintFile; import org.sonarlint.eclipse.core.resource.ISonarLintIssuable; import org.sonarlint.eclipse.core.resource.ISonarLintProject; import org.sonarsource.sonarlint.core.client.api.common.analysis.IssueLocation; +import org.sonarsource.sonarlint.core.client.api.connected.ServerIssue; +import org.sonarsource.sonarlint.core.client.api.connected.ServerIssue.Flow; +import org.sonarsource.sonarlint.core.client.api.connected.ServerIssueLocation; public class SonarLintMarkerUpdater { + private static TaintVulnerabilitiesListener taintVulnerabilitiesListener; + private SonarLintMarkerUpdater() { } + public static void setTaintVulnerabilitiesListener(TaintVulnerabilitiesListener listener) { + taintVulnerabilitiesListener = listener; + } + public static void createOrUpdateMarkers(ISonarLintFile file, Optional openedDocument, Collection issues, TriggerType triggerType) { try { Set previousMarkersToDelete; @@ -77,6 +94,64 @@ public static void createOrUpdateMarkers(ISonarLintFile file, Optional { + List taintVulnerabilities = facade.getServerIssues(binding, currentFile.getProjectRelativePath()) + .stream() + .filter(i -> i.ruleKey().contains("security")) + .filter(i -> StringUtils.isEmpty(i.resolution())) + .collect(Collectors.toList()); + + List boundSiblingProjects = facade.getBoundProjects(binding.projectKey()); + Map bindings = boundSiblingProjects.stream() + .collect(Collectors.toMap(p -> p, p -> SonarLintCorePlugin.loadConfig(p).getProjectBinding().get())); + + for (ServerIssue taintIssue : taintVulnerabilities) { + Optional primaryLocationFile = findFileForLocationInBoundProjects(bindings, taintIssue.getFilePath()); + if (primaryLocationFile.isPresent()) { + createTaintMarker(primaryLocationFile.get().getDocument(), primaryLocationFile.get(), taintIssue, bindings); + } + } + if (!taintVulnerabilities.isEmpty() && taintVulnerabilitiesListener != null) { + taintVulnerabilitiesListener.markersCreated(facade.isSonarCloud()); + } + }); + + } + + public static void deleteTaintMarkers(ISonarLintFile currentFile) { + try { + Set markersToDelete = new HashSet<>(Arrays.asList(currentFile.getResource().findMarkers(SonarLintCorePlugin.MARKER_TAINT_ID, false, IResource.DEPTH_ZERO))); + for (IMarker primaryLocationMarker : markersToDelete) { + MarkerUtils.getIssueFlows(primaryLocationMarker).deleteAllMarkers(); + primaryLocationMarker.delete(); + } + } catch (CoreException e) { + SonarLintLogger.get().error(e.getMessage(), e); + } + } + + private static Optional findFileForLocationInBoundProjects(Map bindingsPerProjects, @Nullable String serverIssuePath) { + if (serverIssuePath == null) { + // Should never occur, no taint issues are at file level + return Optional.empty(); + } + for (Map.Entry entry : bindingsPerProjects.entrySet()) { + Optional idePath = entry.getValue().serverPathToIdePath(serverIssuePath); + if (idePath.isPresent()) { + Optional primaryLocationFile = entry.getKey().find(idePath.get()); + if (primaryLocationFile.isPresent()) { + return primaryLocationFile; + } + } + } + return Optional.empty(); + } + public static Set getResourcesWithMarkers(ISonarLintProject project) throws CoreException { return Arrays.stream(project.getResource().findMarkers(SonarLintCorePlugin.MARKER_ON_THE_FLY_ID, false, IResource.DEPTH_INFINITE)) .map(IMarker::getResource) @@ -136,7 +211,8 @@ private static void createOrUpdateMarkers(ISonarLintFile file, Optional bindingsPerProjects) { + try { + IMarker marker = issuable.getResource().createMarker(SonarLintCorePlugin.MARKER_TAINT_ID); + + setMarkerViewUtilsAttributes(issuable, marker); + + updateMarkerAttributes(document, new ServerIssueTrackable(taintIssue), marker); + createFlowMarkersForTaint(taintIssue, marker, bindingsPerProjects); + } catch (CoreException e) { + SonarLintLogger.get().error("Unable to create marker", e); + } + } + + private static void setMarkerViewUtilsAttributes(ISonarLintIssuable issuable, IMarker marker) throws CoreException { + // See MarkerViewUtil marker.setAttribute("org.eclipse.ui.views.markers.name", issuable.getResourceNameForMarker()); marker.setAttribute("org.eclipse.ui.views.markers.path", issuable.getResourceContainerForMarker()); - - updateMarkerAttributes(document, issuable, trackable, marker, triggerType); } - private static void updateMarkerAttributes(IDocument document, ISonarLintIssuable issuable, Trackable trackable, IMarker marker, TriggerType triggerType) throws CoreException { + private static void updateMarkerAttributes(IDocument document, Trackable trackable, IMarker marker) throws CoreException { Map existingAttributes = marker.getAttributes(); setMarkerAttributeIfDifferent(marker, existingAttributes, MarkerUtils.SONAR_MARKER_RULE_KEY_ATTR, trackable.getRuleKey()); @@ -177,23 +277,23 @@ private static void updateMarkerAttributes(IDocument document, ISonarLintIssuabl setMarkerAttributeIfDifferent(marker, existingAttributes, IMarker.CHAR_END, position.getOffset() + position.getLength()); } - createFlowMarkers(document, issuable, trackable, marker, triggerType); - updateServerMarkerAttributes(trackable, marker); } - private static void createFlowMarkers(IDocument document, ISonarLintIssuable issuable, Trackable trackable, IMarker marker, TriggerType triggerType) throws CoreException { - List flowsMarkers = new ArrayList<>(); + private static void createFlowMarkersForLocalIssues(IDocument document, ISonarLintIssuable issuable, Trackable trackable, IMarker marker, String flowMarkerId) + throws CoreException { + List flows = new ArrayList<>(); int i = 1; for (org.sonarsource.sonarlint.core.client.api.common.analysis.Issue.Flow engineFlow : trackable.getFlows()) { MarkerFlow flow = new MarkerFlow(i); - flowsMarkers.add(flow); + flows.add(flow); List locations = new ArrayList<>(engineFlow.locations()); Collections.reverse(locations); for (IssueLocation l : locations) { + l.getInputFile(); MarkerFlowLocation flowLocation = new MarkerFlowLocation(flow, l.getMessage()); try { - IMarker m = issuable.getResource().createMarker(triggerType.isOnTheFly() ? SonarLintCorePlugin.MARKER_ON_THE_FLY_FLOW_ID : SonarLintCorePlugin.MARKER_REPORT_FLOW_ID); + IMarker m = issuable.getResource().createMarker(flowMarkerId); m.setAttribute(IMarker.MESSAGE, l.getMessage()); m.setAttribute(IMarker.LINE_NUMBER, l.getStartLine() != null ? l.getStartLine() : 1); Position flowPosition = MarkerUtils.getPosition(document, TextRange.get(l.getStartLine(), l.getStartLineOffset(), l.getEndLine(), l.getEndLineOffset())); @@ -208,7 +308,61 @@ private static void createFlowMarkers(IDocument document, ISonarLintIssuable iss } i++; } - marker.setAttribute(MarkerUtils.SONAR_MARKER_EXTRA_LOCATIONS_ATTR, flowsMarkers); + marker.setAttribute(MarkerUtils.SONAR_MARKER_EXTRA_LOCATIONS_ATTR, new MarkerFlows(flows)); + } + + private static void createFlowMarkersForTaint(ServerIssue taintIssue, IMarker primaryLocationMarker, Map bindingsPerProjects) + throws CoreException { + List flows = new ArrayList<>(); + int i = 1; + for (Flow engineFlow : taintIssue.getFlows()) { + MarkerFlow flow = new MarkerFlow(i); + flows.add(flow); + List locations = new ArrayList<>(engineFlow.locations()); + Collections.reverse(locations); + for (ServerIssueLocation l : locations) { + MarkerFlowLocation flowLocation = new MarkerFlowLocation(flow, l.getMessage(), l.getFilePath()); + + Optional locationFile = findFileForLocationInBoundProjects(bindingsPerProjects, l.getFilePath()); + if (!locationFile.isPresent()) { + continue; + } + ISonarLintFile file = locationFile.get(); + try { + IMarker marker = createMarkerIfCodeMatches(file, l); + if (marker != null) { + flowLocation.setMarker(marker); + } else { + flowLocation.setDeleted(true); + } + } catch (Exception e) { + SonarLintLogger.get().debug("Unable to create flow marker", e); + } + } + i++; + } + primaryLocationMarker.setAttribute(MarkerUtils.SONAR_MARKER_EXTRA_LOCATIONS_ATTR, new MarkerFlows(flows)); + } + + @Nullable + private static IMarker createMarkerIfCodeMatches(ISonarLintFile file, ServerIssueLocation location) throws BadLocationException, CoreException { + IDocument document = file.getDocument(); + int startOffset = document.getLineOffset(location.getStartLine() - 1) + location.getStartLineOffset(); + int endOffset = document.getLineOffset(location.getEndLine() - 1) + location.getEndLineOffset(); + String inEditorCode = document.get(startOffset, endOffset - startOffset); + if (inEditorCode.equals(location.getCodeSnippet())) { + IMarker marker = file.getResource().createMarker(SonarLintCorePlugin.MARKER_TAINT_FLOW_ID); + marker.setAttribute(IMarker.MESSAGE, location.getMessage()); + marker.setAttribute(IMarker.LINE_NUMBER, location.getStartLine() != null ? location.getStartLine() : 1); + Position flowPosition = MarkerUtils.getPosition(document, + TextRange.get(location.getStartLine(), location.getStartLineOffset(), location.getEndLine(), location.getEndLineOffset())); + if (flowPosition != null) { + marker.setAttribute(IMarker.CHAR_START, flowPosition.getOffset()); + marker.setAttribute(IMarker.CHAR_END, flowPosition.getOffset() + flowPosition.getLength()); + } + return marker; + } + return null; } /** @@ -265,4 +419,13 @@ public static void deleteAllMarkersFromReport() { p.deleteAllMarkers(SonarLintCorePlugin.MARKER_REPORT_FLOW_ID); }); } + + public static void deleteAllMarkersFromTaint() { + ProjectsProviderUtils.allProjects().stream() + .filter(ISonarLintProject::isOpen) + .forEach(p -> { + p.deleteAllMarkers(SonarLintCorePlugin.MARKER_TAINT_ID); + p.deleteAllMarkers(SonarLintCorePlugin.MARKER_TAINT_FLOW_ID); + }); + } } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlow.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlow.java index 9122345c4..48f9d3fc8 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlow.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlow.java @@ -38,4 +38,11 @@ public List getLocations() { return locations; } + public boolean areAllLocationsInSameFile() { + return locations.stream() + .map(MarkerFlowLocation::getFilePath) + .distinct() + .count() == 1; + } + } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlowLocation.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlowLocation.java index e2a0bb3ec..bfa81ed7f 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlowLocation.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlowLocation.java @@ -20,21 +20,31 @@ package org.sonarlint.eclipse.core.internal.markers; import org.eclipse.core.resources.IMarker; +import org.eclipse.jdt.annotation.Nullable; public class MarkerFlowLocation { private final MarkerFlow parent; private final int number; + @Nullable private final String message; + @Nullable private IMarker marker; private boolean deleted; + @Nullable + private String filePath; - public MarkerFlowLocation(MarkerFlow parent, String message) { + public MarkerFlowLocation(MarkerFlow parent, @Nullable String message) { this.parent = parent; this.parent.locations.add(this); this.number = this.parent.locations.size(); this.message = message; } + public MarkerFlowLocation(MarkerFlow parent, @Nullable String message, @Nullable String filePath) { + this(parent, message); + this.filePath = filePath; + } + public MarkerFlow getParent() { return parent; } @@ -43,6 +53,7 @@ public int getNumber() { return number; } + @Nullable public String getMessage() { return message; } @@ -51,6 +62,7 @@ public void setMarker(IMarker marker) { this.marker = marker; } + @Nullable public IMarker getMarker() { return marker; } @@ -62,4 +74,9 @@ public boolean isDeleted() { public void setDeleted(boolean deleted) { this.deleted = deleted; } + + @Nullable + public String getFilePath() { + return filePath; + } } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlows.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlows.java new file mode 100644 index 000000000..fc4300d27 --- /dev/null +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerFlows.java @@ -0,0 +1,92 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.core.internal.markers; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.eclipse.core.runtime.CoreException; +import org.sonarlint.eclipse.core.SonarLintLogger; + +public class MarkerFlows { + + private final List flows; + + public MarkerFlows(List flows) { + this.flows = flows; + } + + public List getFlows() { + return flows; + } + + public void deleteAllMarkers() { + flows.stream() + .flatMap(f -> f.getLocations().stream()) + .map(MarkerFlowLocation::getMarker) + .filter(Objects::nonNull) + .forEach(m -> { + try { + m.delete(); + } catch (CoreException e) { + SonarLintLogger.get().error(e.getMessage(), e); + } + }); + } + + /** + * Special case when all flows have a single location, this is called "secondary locations" + */ + public boolean isSecondaryLocations() { + return !flows.isEmpty() && flows.stream().allMatch(f -> f.getLocations().size() == 1); + } + + public boolean isEmpty() { + return flows.isEmpty(); + } + + public Stream allLocationsAsStream() { + return flows.stream().flatMap(f -> f.getLocations().stream()); + } + + public int count() { + return flows.size(); + } + + public String getSummaryDescription() { + if (!isEmpty()) { + String kind; + int count; + if (isSecondaryLocations() || count() == 1) { + kind = "location"; + count = (int) flows.stream().flatMap(flow -> flow.locations.stream()).count(); + } else { + kind = "flow"; + count = count(); + } + return " [+" + count + " " + pluralize(kind, count) + "]"; + } + return ""; + } + + private static String pluralize(String str, int count) { + return count == 1 ? str : (str + "s"); + } +} diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerUtils.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerUtils.java index bbc29a636..50d51a486 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerUtils.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/markers/MarkerUtils.java @@ -19,8 +19,10 @@ */ package org.sonarlint.eclipse.core.internal.markers; -import java.util.List; +import java.util.Collections; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; import java.util.function.BiFunction; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; @@ -37,7 +39,7 @@ import org.sonarlint.eclipse.core.internal.preferences.SonarLintGlobalConfiguration; import org.sonarsource.sonarlint.core.client.api.common.RuleKey; -import static java.util.Collections.emptyList; +import static java.util.Arrays.asList; public final class MarkerUtils { @@ -50,6 +52,9 @@ public final class MarkerUtils { public static final String SONAR_MARKER_SERVER_ISSUE_KEY_ATTR = "serverissuekey"; public static final String SONAR_MARKER_EXTRA_LOCATIONS_ATTR = "extralocations"; + public static final Set SONARLINT_PRIMARY_MARKER_IDS = new HashSet<>( + asList(SonarLintCorePlugin.MARKER_ON_THE_FLY_ID, SonarLintCorePlugin.MARKER_REPORT_ID, SonarLintCorePlugin.MARKER_TAINT_ID)); + private MarkerUtils() { } @@ -121,20 +126,12 @@ public static RuleKey getRuleKey(IMarker marker) { return RuleKey.parse(repositoryAndKey); } - public static List getIssueFlows(IMarker marker) { - List flowsMarkers; + public static MarkerFlows getIssueFlows(IMarker marker) { try { - flowsMarkers = Optional.ofNullable((List) marker.getAttribute(SONAR_MARKER_EXTRA_LOCATIONS_ATTR)).orElse(emptyList()); + return Optional.ofNullable((MarkerFlows) marker.getAttribute(SONAR_MARKER_EXTRA_LOCATIONS_ATTR)).orElseGet(() -> new MarkerFlows(Collections.emptyList())); } catch (CoreException e) { - flowsMarkers = emptyList(); + return new MarkerFlows(Collections.emptyList()); } - return flowsMarkers; } - /** - * Special case when all flows have a single location, this is called "secondary locations" - */ - public static boolean isSecondaryLocations(List flowsMarkers) { - return !flowsMarkers.isEmpty() && flowsMarkers.stream().allMatch(f -> f.getLocations().size() == 1); - } } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/preferences/SonarLintGlobalConfiguration.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/preferences/SonarLintGlobalConfiguration.java index 92fedd739..24adfe030 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/preferences/SonarLintGlobalConfiguration.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/preferences/SonarLintGlobalConfiguration.java @@ -78,6 +78,7 @@ public class SonarLintGlobalConfiguration { public static final String PREF_TEST_FILE_REGEXPS_DEFAULT = ""; //$NON-NLS-1$ public static final String PREF_SKIP_CONFIRM_ANALYZE_MULTIPLE_FILES = "skipConfirmAnalyzeMultipleFiles"; //$NON-NLS-1$ public static final String PREF_NODEJS_PATH = "nodeJsPath"; //$NON-NLS-1$ + private static final String PREF_TAINT_VULNERABILITY_DISPLAYED = "taintVulnerabilityDisplayed"; private SonarLintGlobalConfiguration() { // Utility class @@ -313,4 +314,12 @@ public static void setNodeJsPath(String path) { public static String getNodejsPath() { return getPreferenceString(PREF_NODEJS_PATH); } + + public static boolean taintVulnerabilityNeverBeenDisplayed() { + return !getPreferenceBoolean(PREF_TAINT_VULNERABILITY_DISPLAYED); + } + + public static void setTaintVulnerabilityDisplayed() { + setPreferenceBoolean(PREF_TAINT_VULNERABILITY_DISPLAYED, true); + } } diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/telemetry/SonarLintTelemetry.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/telemetry/SonarLintTelemetry.java index 68fcb6a1d..46b30dd27 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/telemetry/SonarLintTelemetry.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/telemetry/SonarLintTelemetry.java @@ -196,6 +196,18 @@ public void showHotspotRequestReceived() { } } + public void taintVulnerabilitiesInvestigatedLocally() { + if (enabled()) { + telemetry.taintVulnerabilitiesInvestigatedLocally(); + } + } + + public void taintVulnerabilitiesInvestigatedRemotely() { + if (enabled()) { + telemetry.taintVulnerabilitiesInvestigatedRemotely(); + } + } + public void stop() { if (scheduledJob != null) { scheduledJob.cancel(); diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueTrackable.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueTrackable.java index 134a588c4..a3bbdf498 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueTrackable.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueTrackable.java @@ -74,7 +74,7 @@ public String getRuleKey() { @Override public String getRuleName() { - throw new UnsupportedOperationException(); + return ""; } @Override diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueUpdater.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueUpdater.java index 9d9e9ed29..2ad1cdb09 100644 --- a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueUpdater.java +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/internal/tracking/ServerIssueUpdater.java @@ -36,6 +36,7 @@ import org.sonarlint.eclipse.core.internal.TriggerType; import org.sonarlint.eclipse.core.internal.engine.connected.ConnectedEngineFacade; import org.sonarlint.eclipse.core.internal.jobs.AsyncServerMarkerUpdaterJob; +import org.sonarlint.eclipse.core.internal.jobs.SonarLintMarkerUpdater; import org.sonarlint.eclipse.core.resource.ISonarLintFile; import org.sonarlint.eclipse.core.resource.ISonarLintIssuable; import org.sonarlint.eclipse.core.resource.ISonarLintProject; @@ -97,6 +98,7 @@ protected IStatus run(IProgressMonitor monitor) { Collection tracked = issueTracker.matchAndTrackServerIssues(file, serverIssuesTrackable); issueTracker.updateCache(file, tracked); trackedIssues.put(issuable, tracked); + SonarLintMarkerUpdater.refreshMarkersForTaint(file, engineFacade); } } if (!trackedIssues.isEmpty()) { diff --git a/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/listener/TaintVulnerabilitiesListener.java b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/listener/TaintVulnerabilitiesListener.java new file mode 100644 index 000000000..1d6b92685 --- /dev/null +++ b/org.sonarlint.eclipse.core/src/org/sonarlint/eclipse/core/listener/TaintVulnerabilitiesListener.java @@ -0,0 +1,24 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.core.listener; + +public interface TaintVulnerabilitiesListener { + void markersCreated(boolean comeFromSonarCloud); +} diff --git a/org.sonarlint.eclipse.ui/build.properties b/org.sonarlint.eclipse.ui/build.properties index 71881feb6..3f2120f71 100644 --- a/org.sonarlint.eclipse.ui/build.properties +++ b/org.sonarlint.eclipse.ui/build.properties @@ -23,4 +23,8 @@ bin.excludes = icons/full/eview16/rule.xcf,\ icons/full/eview16/hotspots@2x.xcf,\ icons/priority/high.xcf,\ icons/priority/low.xcf,\ - icons/priority/medium.xcf + icons/priority/medium.xcf,\ + icons/full/eview16/vulnerabilities.xcf,\ + icons/full/eview16/vulnerabilities@2x.xcf,\ + icons/full/annotation16/vulnerability.xcf,\ + icons/full/annotation16/vulnerability@2x.xcf diff --git a/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.png b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.png new file mode 100644 index 000000000..72f2b636e Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.png differ diff --git a/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.xcf b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.xcf new file mode 100644 index 000000000..f53280c62 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability.xcf differ diff --git a/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.png b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.png new file mode 100644 index 000000000..2ccfde93f Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.png differ diff --git a/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.xcf b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.xcf new file mode 100644 index 000000000..5bac0dcf5 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/annotation16/vulnerability@2x.xcf differ diff --git a/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.png b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.png new file mode 100644 index 000000000..a99007e25 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.png differ diff --git a/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.xcf b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.xcf new file mode 100644 index 000000000..cebd8b1d7 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities.xcf differ diff --git a/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.png b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.png new file mode 100644 index 000000000..b0286a45c Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.png differ diff --git a/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.xcf b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.xcf new file mode 100644 index 000000000..38c6b5316 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/full/eview16/vulnerabilities@2x.xcf differ diff --git a/org.sonarlint.eclipse.ui/icons/sonarcloud-16.png b/org.sonarlint.eclipse.ui/icons/sonarcloud-16.png new file mode 100644 index 000000000..7abe416f2 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/sonarcloud-16.png differ diff --git a/org.sonarlint.eclipse.ui/icons/sonarqube-16.png b/org.sonarlint.eclipse.ui/icons/sonarqube-16.png new file mode 100644 index 000000000..da8464643 Binary files /dev/null and b/org.sonarlint.eclipse.ui/icons/sonarqube-16.png differ diff --git a/org.sonarlint.eclipse.ui/plugin.xml b/org.sonarlint.eclipse.ui/plugin.xml index 8765673d4..d9be1e787 100644 --- a/org.sonarlint.eclipse.ui/plugin.xml +++ b/org.sonarlint.eclipse.ui/plugin.xml @@ -143,6 +143,14 @@ id="org.sonarlint.eclipse.ui.views.HotspotsView" name="SonarLint Security Hotspots"> + + @@ -361,6 +369,96 @@ scope="ON_ANY"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -630,6 +736,43 @@ + + + + + + + + + + + + + + + + @@ -650,6 +793,12 @@ name="Deactivate rule" categoryId="org.sonarlint.eclipse.ui.command.category"> + + + + @@ -781,6 +934,9 @@ + + @@ -866,6 +1046,10 @@ class="org.sonarlint.eclipse.ui.internal.markers.SonarLintMarkerResolutionGenerator" markerType="org.sonarlint.eclipse.core.sonarlintOnTheFlyProblem"> + + + + + + + + + + + diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/DeleteTaintMarkersOnEditorClosed.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/DeleteTaintMarkersOnEditorClosed.java new file mode 100644 index 000000000..c39050f0e --- /dev/null +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/DeleteTaintMarkersOnEditorClosed.java @@ -0,0 +1,80 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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; + +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IPartListener2; +import org.eclipse.ui.IWorkbenchPart; +import org.eclipse.ui.IWorkbenchPartReference; +import org.sonarlint.eclipse.core.internal.adapter.Adapters; +import org.sonarlint.eclipse.core.internal.jobs.SonarLintMarkerUpdater; +import org.sonarlint.eclipse.core.resource.ISonarLintFile; + +public class DeleteTaintMarkersOnEditorClosed implements IPartListener2 { + @Override + public void partOpened(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partVisible(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partInputChanged(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partHidden(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partDeactivated(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partClosed(IWorkbenchPartReference partRef) { + IWorkbenchPart part = partRef.getPart(true); + if (part instanceof IEditorPart) { + IEditorInput input = ((IEditorPart) part).getEditorInput(); + if (input instanceof IFileEditorInput) { + ISonarLintFile sonarLintFile = Adapters.adapt(input, ISonarLintFile.class); + SonarLintMarkerUpdater.deleteTaintMarkers(sonarLintFile); + } + } + } + + @Override + public void partBroughtToTop(IWorkbenchPartReference partRef) { + // Nothing to do + } + + @Override + public void partActivated(IWorkbenchPartReference partRef) { + // Nothing to do + } + +} diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintImages.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintImages.java index 22656c059..0e151b38f 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintImages.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintImages.java @@ -43,6 +43,7 @@ public final class SonarLintImages { public static final Image ISSUE_ANNOTATION = createImage("full/annotation16/issue.png"); //$NON-NLS-1$ public static final Image HOTSPOT_ANNOTATION = createImage("full/annotation16/hotspot.png"); //$NON-NLS-1$ + public static final Image VULNERABILITY_ANNOTATION = createImage("full/annotation16/vulnerability.png"); //$NON-NLS-1$ public static final Image RESOLUTION_SHOW_RULE = createImage("full/marker_resolution16/showrule.png"); //$NON-NLS-1$ public static final Image RESOLUTION_SHOW_LOCATIONS = createImage("full/marker_resolution16/showlocations.png"); //$NON-NLS-1$ public static final Image RESOLUTION_DISABLE_RULE = createImage("full/marker_resolution16/disablerule.png"); //$NON-NLS-1$ @@ -66,6 +67,9 @@ public final class SonarLintImages { public static final Image IMG_SONARQUBE_LOGO = createImage("logo/sonarqube-black-256px.png"); //$NON-NLS-1$ public static final Image IMG_SONARCLOUD_LOGO = createImage("logo/sonarcloud-black-256px.png"); //$NON-NLS-1$ + public static final ImageDescriptor SONARCLOUD_16 = createImageDescriptor("sonarcloud-16.png"); //$NON-NLS-1$ + public static final ImageDescriptor SONARQUBE_16 = createImageDescriptor("sonarqube-16.png"); //$NON-NLS-1$ + public static final Image IMG_OPEN_EXTERNAL = createImage("external-link-16.png"); //$NON-NLS-1$ public static final ImageDescriptor DEBUG = createImageDescriptor("debug.gif"); //$NON-NLS-1$ diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintUiPlugin.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintUiPlugin.java index 851c5d85e..56048cbc0 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintUiPlugin.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/SonarLintUiPlugin.java @@ -43,6 +43,7 @@ import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin; import org.sonarlint.eclipse.core.internal.TriggerType; import org.sonarlint.eclipse.core.internal.engine.connected.IConnectedEngineFacade; +import org.sonarlint.eclipse.core.internal.jobs.SonarLintMarkerUpdater; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.internal.notifications.ListenerFactory; import org.sonarlint.eclipse.core.internal.preferences.SonarLintGlobalConfiguration; @@ -58,6 +59,7 @@ import org.sonarlint.eclipse.ui.internal.popup.GenericNotificationPopup; import org.sonarlint.eclipse.ui.internal.popup.MissingNodePopup; import org.sonarlint.eclipse.ui.internal.popup.ServerStorageNeedUpdatePopup; +import org.sonarlint.eclipse.ui.internal.popup.TaintVulnerabilityAvailablePopup; import org.sonarsource.sonarlint.core.client.api.connected.ConnectedSonarLintEngine.State; import org.sonarsource.sonarlint.core.client.api.notifications.ServerNotification; import org.sonarsource.sonarlint.core.client.api.notifications.ServerNotificationListener; @@ -164,11 +166,24 @@ public void start(final BundleContext context) throws Exception { getPreferenceStore().addPropertyChangeListener(prefListener); + SonarLintMarkerUpdater.setTaintVulnerabilitiesListener(SonarLintUiPlugin::notifyTaintVulnerabilitiesDisplayed); + new CheckForUpdatesJob().schedule((long) 10 * 1000); startupAsync(); } + private static void notifyTaintVulnerabilitiesDisplayed(boolean comeFromSonarCloud) { + if (SonarLintGlobalConfiguration.taintVulnerabilityNeverBeenDisplayed()) { + SonarLintGlobalConfiguration.setTaintVulnerabilityDisplayed(); + Display.getDefault().syncExec(() -> showTaintVulnerabitilityNotification(comeFromSonarCloud)); + } + } + + private static void showTaintVulnerabitilityNotification(boolean comeFromSonarCloud) { + new TaintVulnerabilityAvailablePopup(comeFromSonarCloud).open(); + } + @Override public void stop(final BundleContext context) throws Exception { hotspotsHandlerServer.shutdown(); diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/WindowOpenCloseListener.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/WindowOpenCloseListener.java index 5954c3541..ae24ed829 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/WindowOpenCloseListener.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/WindowOpenCloseListener.java @@ -28,6 +28,7 @@ class WindowOpenCloseListener implements IWindowListener { private static final OpenEditorAnalysisTrigger OPEN_EDITOR_ANALYSIS_TRIGGER = new OpenEditorAnalysisTrigger(); + private static final DeleteTaintMarkersOnEditorClosed DELETE_TAINT_MARKERS_ON_EDITOR_CLOSED = new DeleteTaintMarkersOnEditorClosed(); private static final IPageListener PAGE_OPEN_CLOSE_LISTENER = new IPageListener() { @@ -79,6 +80,7 @@ static void addListenerToAllPages(IWorkbenchWindow window) { private static void addListenersToPage(IWorkbenchPage page) { page.addPartListener(OPEN_EDITOR_ANALYSIS_TRIGGER); + page.addPartListener(DELETE_TAINT_MARKERS_ON_EDITOR_CLOSED); page.addPartListener(SonarLintFlowAnnotator.PART_LISTENER); page.addPostSelectionListener(SonarLintUiPlugin.getSonarlintMarkerSelectionService()); } @@ -91,6 +93,7 @@ static void removeListenerFromAllPages(IWorkbenchWindow window) { private static void removeListenersFromPage(IWorkbenchPage page) { page.removePartListener(OPEN_EDITOR_ANALYSIS_TRIGGER); + page.removePartListener(DELETE_TAINT_MARKERS_ON_EDITOR_CLOSED); page.removePartListener(SonarLintFlowAnnotator.PART_LISTENER); page.removePostSelectionListener(SonarLintUiPlugin.getSonarlintMarkerSelectionService()); } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintCodeMiningProvider.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintCodeMiningProvider.java index 387a459a4..72c267c0c 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintCodeMiningProvider.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintCodeMiningProvider.java @@ -46,6 +46,7 @@ import org.sonarlint.eclipse.core.SonarLintLogger; import org.sonarlint.eclipse.core.internal.markers.MarkerFlow; import org.sonarlint.eclipse.core.internal.markers.MarkerFlowLocation; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.ui.internal.SonarLintUiPlugin; import org.sonarlint.eclipse.ui.internal.flowlocations.SonarLintFlowLocationSelectionListener; @@ -133,10 +134,7 @@ public void dispose() { @Override public void markerSelected(Optional marker) { - forceRefreshCodeMiningsIfNecessary(marker, m -> { - List flowsMarkers = MarkerUtils.getIssueFlows(m); - return flowsMarkers.stream().flatMap(f -> f.getLocations().stream()); - }); + forceRefreshCodeMiningsIfNecessary(marker, m -> MarkerUtils.getIssueFlows(m).allLocationsAsStream()); } @Override @@ -181,17 +179,13 @@ public CompletableFuture> provideCodeMinings(ITextVi if (markerToUse == null) { return CompletableFuture.completedFuture(emptyList()); } - // Return fast if the marker is not for the current editor ITextEditor textEditor = super.getAdapter(ITextEditor.class); IFileEditorInput editorInput = textEditor.getEditorInput().getAdapter(IFileEditorInput.class); - if (editorInput == null || !editorInput.getFile().equals(markerToUse.getResource())) { - return CompletableFuture.completedFuture(emptyList()); - } - List flowsMarkers = MarkerUtils.getIssueFlows(markerToUse); + MarkerFlows flowsMarkers = MarkerUtils.getIssueFlows(markerToUse); if (flowsMarkers.isEmpty()) { return CompletableFuture.completedFuture(emptyList()); } - boolean isSecondaryLocation = MarkerUtils.isSecondaryLocations(flowsMarkers); + boolean isSecondaryLocation = flowsMarkers.isSecondaryLocations(); Optional lastSelectedFlow = SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedFlow(); if (!isSecondaryLocation && !lastSelectedFlow.isPresent()) { return CompletableFuture.completedFuture(emptyList()); @@ -204,7 +198,7 @@ public CompletableFuture> provideCodeMinings(ITextVi List locations; if (isSecondaryLocation) { // Flatten all locations - locations = flowsMarkers.stream().flatMap(f -> f.getLocations().stream()).collect(toList()); + locations = flowsMarkers.allLocationsAsStream().collect(toList()); } else if (lastSelectedFlow.isPresent()) { locations = lastSelectedFlow.get().getLocations(); } else { @@ -222,13 +216,15 @@ private List createMiningsForLocations(ITextEditor textEditor, List List result = new ArrayList<>(); for (MarkerFlowLocation l : locations) { try { - @Nullable - Position position = LocationsUtils.getMarkerPosition(l.getMarker(), textEditor); - if (position != null && !position.isDeleted()) { - result.add(new SonarLintFlowMessageCodeMining(l, doc, position, this)); - result.add( - new SonarLintFlowLocationNumberCodeMining(l, position, this, number, - l.equals(SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedFlowLocation().orElse(null)))); + IMarker marker = l.getMarker(); + if (marker != null && !l.isDeleted()) { + Position position = LocationsUtils.getMarkerPosition(marker, textEditor); + if (position != null && !position.isDeleted()) { + result.add(new SonarLintFlowMessageCodeMining(l, doc, position, this)); + result.add( + new SonarLintFlowLocationNumberCodeMining(l, position, this, number, + l.equals(SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedFlowLocation().orElse(null)))); + } } number++; } catch (BadLocationException e) { diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintFlowLocationNumberCodeMining.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintFlowLocationNumberCodeMining.java index a3f15bc34..798b25bbf 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintFlowLocationNumberCodeMining.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/codemining/SonarLintFlowLocationNumberCodeMining.java @@ -68,7 +68,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { gc.setAntialias(SWT.ON); String numberStr = Integer.toString(number); Point numberExtent = gc.stringExtent(numberStr); - // Compute all sizes based on text size to adapt to zoom in/ou + // Compute all sizes based on text size to adapt to zoom in/out int arcRadius = (int) (numberExtent.y * ARC_RADIUS_RATIO); int horizontalPadding = (int) (numberExtent.y * HORIZONTAL_PADDING_RATIO); int horizontalMargin = (int) (numberExtent.y * HORIZONTAL_MARGIN_RATIO); diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/AbstractIssueCommand.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/AbstractIssueCommand.java index 7a3af4897..30523b7fc 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/AbstractIssueCommand.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/AbstractIssueCommand.java @@ -45,10 +45,7 @@ public Display getDisplay() { } @Nullable - @Override - public Object execute(ExecutionEvent event) throws ExecutionException { - IStructuredSelection selection = (IStructuredSelection) HandlerUtil.getCurrentSelectionChecked(event); - + protected static IMarker getSelectedMarker(IStructuredSelection selection) { List selectedSonarMarkers = new ArrayList<>(); @SuppressWarnings("rawtypes") @@ -59,12 +56,16 @@ public Object execute(ExecutionEvent event) throws ExecutionException { selectedSonarMarkers.add(marker); } } + return !selectedSonarMarkers.isEmpty() ? selectedSonarMarkers.get(0) : null; + } - if (!selectedSonarMarkers.isEmpty()) { - IMarker marker = selectedSonarMarkers.get(0); + @Nullable + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + IMarker marker = getSelectedMarker((IStructuredSelection) HandlerUtil.getCurrentSelectionChecked(event)); + if (marker != null) { execute(marker); } - return null; } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/OpenInBrowserCommand.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/OpenInBrowserCommand.java new file mode 100644 index 000000000..b0d7c4ab3 --- /dev/null +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/command/OpenInBrowserCommand.java @@ -0,0 +1,82 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.command; + +import java.net.URL; +import java.util.Map; +import java.util.Optional; +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.commands.IElementUpdater; +import org.eclipse.ui.menus.UIElement; +import org.sonarlint.eclipse.core.SonarLintLogger; +import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin; +import org.sonarlint.eclipse.core.internal.adapter.Adapters; +import org.sonarlint.eclipse.core.internal.engine.connected.ResolvedBinding; +import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; +import org.sonarlint.eclipse.core.internal.utils.StringUtils; +import org.sonarlint.eclipse.core.resource.ISonarLintProject; +import org.sonarlint.eclipse.ui.internal.SonarLintImages; + +public class OpenInBrowserCommand extends AbstractIssueCommand implements IElementUpdater { + + @Override + public void updateElement(UIElement element, Map parameters) { + IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + if (window != null) { + IStructuredSelection selection = (IStructuredSelection) window.getSelectionService().getSelection(); + Optional binding = getBinding(getSelectedMarker(selection)); + if (binding.isPresent()) { + element.setIcon(binding.get().getEngineFacade().isSonarCloud() ? SonarLintImages.SONARCLOUD_16 : SonarLintImages.SONARQUBE_16); + } + } + } + + @Override + protected void execute(IMarker selectedMarker) { + try { + Optional binding = getBinding(selectedMarker); + if (!binding.isPresent()) { + SonarLintLogger.get().info("Unable to open issue in browser: project is not bound"); + return; + } + SonarLintCorePlugin.getTelemetry().taintVulnerabilitiesInvestigatedRemotely(); + String issueKey = (String) selectedMarker.getAttribute(MarkerUtils.SONAR_MARKER_SERVER_ISSUE_KEY_ATTR); + String serverIssueLink = buildLink(binding.get().getEngineFacade().getHost(), binding.get().getProjectBinding().projectKey(), issueKey); + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL(serverIssueLink)); + } catch (Exception e) { + SonarLintLogger.get().error("Unable to open issue in browser", e); + } + } + + private static Optional getBinding(IMarker marker) { + ISonarLintProject project = Adapters.adapt(marker.getResource().getProject(), ISonarLintProject.class); + return SonarLintCorePlugin.getServersManager().resolveBinding(project); + } + + private static String buildLink(String serverUrl, String projectKey, String issueKey) { + String urlEncodedProjectKey = StringUtils.urlEncode(projectKey); + String urlEncodedIssueKey = StringUtils.urlEncode(issueKey); + return serverUrl + "/project/issues?id=" + urlEncodedProjectKey + "&open=" + urlEncodedIssueKey; + } + +} diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowAnnotator.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowAnnotator.java index ad0e89793..7ecfaecc2 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowAnnotator.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowAnnotator.java @@ -42,6 +42,7 @@ import org.eclipse.ui.texteditor.ITextEditor; import org.sonarlint.eclipse.core.internal.markers.MarkerFlow; import org.sonarlint.eclipse.core.internal.markers.MarkerFlowLocation; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.ui.internal.SonarLintUiPlugin; import org.sonarlint.eclipse.ui.internal.util.LocationsUtils; @@ -123,9 +124,9 @@ public SonarLintFlowAnnotator(ITextEditor textEditor) { public void documentChanged(DocumentEvent event) { Optional lastSelectedMarker = SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedMarker(); if (lastSelectedMarker.isPresent()) { - List issueFlows = MarkerUtils.getIssueFlows(lastSelectedMarker.get()); + MarkerFlows issueFlows = MarkerUtils.getIssueFlows(lastSelectedMarker.get()); IssueLocationsView view = (IssueLocationsView) PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().findView(IssueLocationsView.ID); - issueFlows.stream().flatMap(f -> f.getLocations().stream()).forEach(l -> { + issueFlows.allLocationsAsStream().forEach(l -> { Position markerPosition = LocationsUtils.getMarkerPosition(l.getMarker(), textEditor); if (markerPosition != null && markerPosition.isDeleted() != l.isDeleted()) { l.setDeleted(markerPosition.isDeleted()); @@ -192,16 +193,15 @@ private static Map createAnnotations(ITextEditor textEdito if (markerToUse == null) { return emptyMap(); } - List flowsMarkers = MarkerUtils.getIssueFlows(markerToUse); + MarkerFlows flowsMarkers = MarkerUtils.getIssueFlows(markerToUse); if (flowsMarkers.isEmpty()) { return emptyMap(); } - boolean isSecondaryLocation = MarkerUtils.isSecondaryLocations(flowsMarkers); Optional lastSelectedFlow = SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedFlow(); List locations; - if (isSecondaryLocation) { + if (flowsMarkers.isSecondaryLocations()) { // Flatten all locations - locations = flowsMarkers.stream().flatMap(f -> f.getLocations().stream()).collect(toList()); + locations = flowsMarkers.allLocationsAsStream().collect(toList()); } else if (lastSelectedFlow.isPresent()) { locations = lastSelectedFlow.get().getLocations(); } else { @@ -209,11 +209,14 @@ private static Map createAnnotations(ITextEditor textEdito } Map result = new HashMap<>(); locations.forEach(location -> { - Position markerPosition = LocationsUtils.getMarkerPosition(location.getMarker(), textEditor); - if (markerPosition != null && !markerPosition.isDeleted()) { - Annotation annotation = new Annotation(ISSUE_FLOW_ANNOTATION_TYPE, false, location.getMessage()); - // Copy the position to avoid having it updated twice when document is updated - result.put(annotation, new Position(markerPosition.getOffset(), markerPosition.getLength())); + IMarker marker = location.getMarker(); + if (marker != null && !location.isDeleted()) { + Position markerPosition = LocationsUtils.getMarkerPosition(marker, textEditor); + if (markerPosition != null && !markerPosition.isDeleted()) { + Annotation annotation = new Annotation(ISSUE_FLOW_ANNOTATION_TYPE, false, location.getMessage()); + // Copy the position to avoid having it updated twice when document is updated + result.put(annotation, new Position(markerPosition.getOffset(), markerPosition.getLength())); + } } }); return result; diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowLocationsService.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowLocationsService.java index 0f98a5897..0bc36ae59 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowLocationsService.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/flowlocations/SonarLintFlowLocationsService.java @@ -21,12 +21,12 @@ import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.viewers.ISelection; import org.eclipse.swt.widgets.Display; @@ -37,14 +37,17 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; 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.MarkerFlow; import org.sonarlint.eclipse.core.internal.markers.MarkerFlowLocation; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.ui.internal.util.SelectionUtils; import org.sonarlint.eclipse.ui.internal.views.issues.OnTheFlyIssuesView; import org.sonarlint.eclipse.ui.internal.views.issues.SonarLintReportView; +import org.sonarlint.eclipse.ui.internal.views.issues.TaintVulnerabilitiesView; import org.sonarlint.eclipse.ui.internal.views.locations.IssueLocationsView; public class SonarLintFlowLocationsService implements ISelectionListener, AnalysisListener { @@ -57,7 +60,7 @@ public class SonarLintFlowLocationsService implements ISelectionListener, Analys private Optional lastSelectedFlowLocation = Optional.empty(); private boolean showAnnotationsInEditor = true; - private static final Set sonarlintMarkerViewsIds = new HashSet<>(Arrays.asList(SonarLintReportView.ID, OnTheFlyIssuesView.ID)); + private static final Set sonarlintMarkerViewsIds = new HashSet<>(Arrays.asList(SonarLintReportView.ID, OnTheFlyIssuesView.ID, TaintVulnerabilitiesView.ID)); @Override public void selectionChanged(IWorkbenchPart part, ISelection selection) { @@ -76,11 +79,11 @@ public void usedAnalysis(AnalysisEvent event) { // Marker has been deleted during the last analysis markerSelected(null, false, false); } else { - List newIssueFlows = MarkerUtils.getIssueFlows(lastSelectedMarker.get()); + MarkerFlows newIssueFlows = MarkerUtils.getIssueFlows(lastSelectedMarker.get()); // Try to reselect the same flow number than before Integer pastFlowNum = lastSelectedFlow.map(MarkerFlow::getNumber).orElse(null); - if (pastFlowNum != null && newIssueFlows.size() >= pastFlowNum) { - lastSelectedFlow = Optional.of(newIssueFlows.get(pastFlowNum - 1)); + if (pastFlowNum != null && newIssueFlows.count() >= pastFlowNum) { + lastSelectedFlow = Optional.of(newIssueFlows.getFlows().get(pastFlowNum - 1)); } // Try to select the same flow location Integer pastFlowLocationNum = lastSelectedFlowLocation.map(MarkerFlowLocation::getNumber).orElse(null); @@ -159,11 +162,12 @@ public void markerSelected(@Nullable IMarker selectedMarker, boolean forceShowAn lastSelectedFlow = Optional.empty(); lastSelectedFlowLocation = Optional.empty(); if (selectedMarker != null) { - List issueFlow = MarkerUtils.getIssueFlows(selectedMarker); - if (!MarkerUtils.isSecondaryLocations(issueFlow) && !issueFlow.isEmpty()) { + MarkerFlows issueFlow = MarkerUtils.getIssueFlows(selectedMarker); + if (!issueFlow.isSecondaryLocations() && !issueFlow.isEmpty()) { // Select the first flow - lastSelectedFlow = Optional.of(issueFlow.get(0)); + lastSelectedFlow = Optional.of(issueFlow.getFlows().get(0)); } + triggerTelemetryOnShowTaintVulnerability(selectedMarker); } notifyAllOfMarkerChange(); } @@ -201,4 +205,14 @@ public void setShowAnnotationsInEditor(boolean enabled) { notifyAllOfMarkerChange(); } } + + private static void triggerTelemetryOnShowTaintVulnerability(IMarker marker) { + try { + if (marker.getType().equals(SonarLintCorePlugin.MARKER_TAINT_ID)) { + SonarLintCorePlugin.getTelemetry().taintVulnerabilitiesInvestigatedLocally(); + } + } catch (CoreException e) { + SonarLintLogger.get().debug("Cannot get marker type", e); + } + } } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/ShowHideIssueFlowsMarkerResolver.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/ShowHideIssueFlowsMarkerResolver.java index 6afb029f5..4dde762f7 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/ShowHideIssueFlowsMarkerResolver.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/ShowHideIssueFlowsMarkerResolver.java @@ -35,7 +35,7 @@ public class ShowHideIssueFlowsMarkerResolver implements IMarkerResolution2 { public ShowHideIssueFlowsMarkerResolver(IMarker marker) { this.marker = marker; this.alreadySelected = marker.equals(SonarLintUiPlugin.getSonarlintMarkerSelectionService().getLastSelectedMarker().orElse(null)); - isSecondaryLocation = MarkerUtils.isSecondaryLocations(MarkerUtils.getIssueFlows(marker)); + isSecondaryLocation = MarkerUtils.getIssueFlows(marker).isSecondaryLocations(); } @Override diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerImageProvider.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerImageProvider.java index e495e105e..1c85fd945 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerImageProvider.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerImageProvider.java @@ -42,6 +42,9 @@ public Image getManagedImage(Annotation annotation) { if (annotation.getType().equals("org.sonarlint.eclipse.hotspotAnnotationType")) { return SonarLintImages.HOTSPOT_ANNOTATION; } + if (annotation.getType().equals("org.sonarlint.eclipse.taintAnnotationType")) { + return SonarLintImages.VULNERABILITY_ANNOTATION; + } return SonarLintImages.ISSUE_ANNOTATION; } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerResolutionGenerator.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerResolutionGenerator.java index 2ab9be5bf..770b6a69c 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerResolutionGenerator.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/markers/SonarLintMarkerResolutionGenerator.java @@ -56,7 +56,7 @@ public IMarkerResolution[] getResolutions(final IMarker marker) { private static boolean isSonarLintIssueMarker(IMarker marker) { try { - return SonarLintCorePlugin.MARKER_ON_THE_FLY_ID.equals(marker.getType()) || SonarLintCorePlugin.MARKER_REPORT_ID.equals(marker.getType()); + return MarkerUtils.SONARLINT_PRIMARY_MARKER_IDS.contains(marker.getType()); } catch (final CoreException e) { return false; } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/popup/TaintVulnerabilityAvailablePopup.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/popup/TaintVulnerabilityAvailablePopup.java new file mode 100644 index 000000000..25def7d60 --- /dev/null +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/popup/TaintVulnerabilityAvailablePopup.java @@ -0,0 +1,68 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.popup; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.sonarlint.eclipse.core.SonarLintLogger; +import org.sonarlint.eclipse.ui.internal.SonarLintImages; +import org.sonarlint.eclipse.ui.internal.views.issues.TaintVulnerabilitiesView; + +public class TaintVulnerabilityAvailablePopup extends AbstractSonarLintPopup { + + private boolean comeFromSonarCloud; + + public TaintVulnerabilityAvailablePopup(boolean comeFromSonarCloud) { + this.comeFromSonarCloud = comeFromSonarCloud; + } + + @Override + protected String getMessage() { + return "Taint vulnerabilities have been detected by " + (comeFromSonarCloud ? "SonarCloud" : "SonarQube") + + " on this file. SonarLint can show you those vulnerabilities in your local code."; + } + + @Override + protected void createContentArea(Composite composite) { + super.createContentArea(composite); + + addLink("Show in view", e -> Display.getDefault().asyncExec(() -> { + close(); + try { + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().showView(TaintVulnerabilitiesView.ID); + } catch (PartInitException exception) { + SonarLintLogger.get().debug("Cannot show taint vulnerabilitites view", exception); + } + })); + } + + @Override + protected String getPopupShellTitle() { + return "SonarLint - Taint vulnerability found"; + } + + @Override + protected Image getPopupShellImage(int maximumHeight) { + return SonarLintImages.BALLOON_IMG; + } +} diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/properties/AboutPropertyPage.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/properties/AboutPropertyPage.java index ffcc0520c..8a3054d3c 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/properties/AboutPropertyPage.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/properties/AboutPropertyPage.java @@ -92,6 +92,10 @@ protected Control createContents(final Composite parent) { " },\n" + " \"show_hotspot\": {\n" + " \"requests_count\": 3\n" + + " },\n" + + " \"taint_vulnerabilities\": {\n" + + " \"investigated_locally_count\": 3,\n" + + " \"investigated_remotely_count\": 4\n" + " }\n" + "}"); diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/util/SelectionUtils.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/util/SelectionUtils.java index f1ce313a9..0f3a5cdb0 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/util/SelectionUtils.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/util/SelectionUtils.java @@ -31,8 +31,8 @@ import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.handlers.HandlerUtil; -import org.sonarlint.eclipse.core.internal.SonarLintCorePlugin; import org.sonarlint.eclipse.core.internal.adapter.Adapters; +import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.resource.ISonarLintFile; import org.sonarlint.eclipse.core.resource.ISonarLintProject; import org.sonarlint.eclipse.core.resource.ISonarLintProjectContainer; @@ -160,7 +160,7 @@ private static void processElement(List selectedSonarMarkers, Object el } private static boolean isSonarLintMarker(IMarker marker) throws CoreException { - return SonarLintCorePlugin.MARKER_ON_THE_FLY_ID.equals(marker.getType()) || SonarLintCorePlugin.MARKER_REPORT_ID.equals(marker.getType()); + return MarkerUtils.SONARLINT_PRIMARY_MARKER_IDS.contains(marker.getType()); } } diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/IssueDescriptionField.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/IssueDescriptionField.java index a63659188..5bbb33399 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/IssueDescriptionField.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/IssueDescriptionField.java @@ -19,7 +19,6 @@ */ package org.sonarlint.eclipse.ui.internal.views.issues; -import java.util.List; import java.util.Locale; import org.eclipse.core.resources.IMarker; import org.eclipse.jdt.annotation.Nullable; @@ -29,7 +28,7 @@ import org.eclipse.swt.widgets.Control; import org.eclipse.ui.views.markers.MarkerField; import org.eclipse.ui.views.markers.MarkerItem; -import org.sonarlint.eclipse.core.internal.markers.MarkerFlow; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.internal.utils.CompatibilityUtils; import org.sonarlint.eclipse.ui.internal.SonarLintImages; @@ -65,25 +64,12 @@ public String getValue(MarkerItem item) { IMarker marker = item.getMarker(); // When grouping by severity, MarkerItem will be a MarkerCategory, that doesn't have an attached marker if (marker != null) { - List issueFlows = MarkerUtils.getIssueFlows(marker); - if (!issueFlows.isEmpty()) { - boolean isSecondary = MarkerUtils.isSecondaryLocations(issueFlows); - String kind; - if (isSecondary) { - kind = "location"; - } else { - kind = "flow"; - } - sb.append(" [+").append(issueFlows.size()).append(" ").append(pluralize(kind, issueFlows.size())).append("]"); - } + MarkerFlows issueFlows = MarkerUtils.getIssueFlows(marker); + sb.append(issueFlows.getSummaryDescription()); } return sb.toString(); } - private static String pluralize(String str, int count) { - return count == 1 ? str : (str + "s"); - } - @Override public int compare(MarkerItem item1, MarkerItem item2) { int severity1 = getSeverity(item1); diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/TaintVulnerabilitiesView.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/TaintVulnerabilitiesView.java new file mode 100644 index 000000000..91e0e07bd --- /dev/null +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/issues/TaintVulnerabilitiesView.java @@ -0,0 +1,61 @@ +/* + * SonarLint for Eclipse + * Copyright (C) 2015-2021 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.issues; + +import java.net.MalformedURLException; +import java.net.URL; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Link; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.sonarlint.eclipse.core.SonarLintLogger; +import org.sonarlint.eclipse.ui.internal.SonarLintUiPlugin; + +public class TaintVulnerabilitiesView extends MarkerViewWithBottomPanel { + + public static final String ID = SonarLintUiPlugin.PLUGIN_ID + ".views.issues.TaintVulnerabilitiesView"; + + public TaintVulnerabilitiesView() { + super(SonarLintUiPlugin.PLUGIN_ID + ".views.issues.taintIssueMarkerGenerator"); + } + + @Override + protected void populateBottomPanel(Composite bottom) { + RowLayout bottomLayout = new RowLayout(); + bottomLayout.center = true; + bottom.setLayout(bottomLayout); + GridData bottomLayoutData = new GridData(SWT.FILL, SWT.FILL, true, false); + bottom.setLayoutData(bottomLayoutData); + + Link label = new Link(bottom, SWT.NONE); + label.setText("This view displays taint vulnerabilities found by SonarQube or SonarCloud on the main branch during last analysis. Learn more"); + label.addListener(SWT.Selection, e -> { + try { + PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(new URL("https://github.com/SonarSource/sonarlint-eclipse/wiki/Taint-Vulnerabilities")); + } catch (PartInitException | MalformedURLException ex) { + SonarLintLogger.get().error("Unable to open the browser", ex); + } + }); + } + +} diff --git a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/locations/IssueLocationsView.java b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/locations/IssueLocationsView.java index 970bf65ed..f3b7c9a76 100644 --- a/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/locations/IssueLocationsView.java +++ b/org.sonarlint.eclipse.ui/src/org/sonarlint/eclipse/ui/internal/views/locations/IssueLocationsView.java @@ -19,6 +19,8 @@ */ package org.sonarlint.eclipse.ui.internal.views.locations; +import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -51,6 +53,7 @@ import org.sonarlint.eclipse.core.SonarLintLogger; import org.sonarlint.eclipse.core.internal.markers.MarkerFlow; import org.sonarlint.eclipse.core.internal.markers.MarkerFlowLocation; +import org.sonarlint.eclipse.core.internal.markers.MarkerFlows; import org.sonarlint.eclipse.core.internal.markers.MarkerUtils; import org.sonarlint.eclipse.core.internal.utils.StringUtils; import org.sonarlint.eclipse.ui.internal.SonarLintImages; @@ -71,12 +74,16 @@ public class IssueLocationsView extends ViewPart implements SonarLintMarkerSelec private ToggleAnnotationsAction showAnnotationsAction; - private static class FlowNode { + private interface LocationNode { + boolean isValid(); + } + + private static class FlowLocationNode implements LocationNode { private final String label; private final MarkerFlowLocation location; - public FlowNode(MarkerFlowLocation location) { + public FlowLocationNode(MarkerFlowLocation location) { this.label = location.getParent().getLocations().size() > 1 ? (location.getNumber() + ": " + location.getMessage()) : location.getMessage(); this.location = location; } @@ -89,6 +96,12 @@ public MarkerFlowLocation getLocation() { return location; } + @Override + public boolean isValid() { + IMarker marker = location.getMarker(); + return marker != null && marker.exists() && !location.isDeleted(); + } + @Override public int hashCode() { return Objects.hash(location.getParent().getNumber(), location.getNumber()); @@ -99,10 +112,10 @@ public boolean equals(Object obj) { if (this == obj) { return true; } - if (!(obj instanceof FlowNode)) { + if (!(obj instanceof FlowLocationNode)) { return false; } - FlowNode other = (FlowNode) obj; + FlowLocationNode other = (FlowLocationNode) obj; return Objects.equals(location.getParent().getNumber(), other.location.getParent().getNumber()) && Objects.equals(location.getNumber(), other.location.getNumber()); } @@ -110,16 +123,28 @@ public boolean equals(Object obj) { private static class FlowRootNode { - private final List children; + private final List children; private final MarkerFlow flow; public FlowRootNode(MarkerFlow flow) { this.flow = flow; - children = flow.getLocations().stream() - // SLE-388 - "Highlight-only" locations don't have a message - .filter(l -> !StringUtils.isEmpty(l.getMessage())) - .map(FlowNode::new) - .collect(toList()); + if (flow.areAllLocationsInSameFile()) { + children = flow.getLocations().stream() + // SLE-388 - "Highlight-only" locations don't have a message + .filter(l -> !StringUtils.isEmpty(l.getMessage())) + .map(FlowLocationNode::new) + .collect(toList()); + } else { + children = new ArrayList<>(); + LocationFileGroupNode lastNode = null; + for (MarkerFlowLocation location : flow.getLocations()) { + if (lastNode == null || !lastNode.getFilePath().equals(location.getFilePath())) { + lastNode = new LocationFileGroupNode(children.size(), location.getFilePath()); + children.add(lastNode); + } + lastNode.addLocation(new FlowLocationNode(location)); + } + } } public MarkerFlow getFlow() { @@ -130,8 +155,8 @@ public String getLabel() { return "Flow " + flow.getNumber(); } - public FlowNode[] getChildren() { - return children.toArray(new FlowNode[0]); + public LocationNode[] getChildren() { + return children.toArray(new LocationNode[0]); } @Override @@ -153,22 +178,69 @@ public boolean equals(Object obj) { } + private static class LocationFileGroupNode implements LocationNode { + + private final int groupIndex; + private final String filePath; + private List children = new ArrayList<>(); + + public LocationFileGroupNode(int groupIndex, String filePath) { + this.groupIndex = groupIndex; + this.filePath = filePath; + } + + public void addLocation(FlowLocationNode flowLocationNode) { + children.add(flowLocationNode); + } + + public @Nullable String getFilePath() { + return filePath; + } + + public List getChildren() { + return children; + } + + @Override + public boolean isValid() { + return children.get(0).isValid(); + } + + @Override + public int hashCode() { + return Objects.hash(filePath, groupIndex); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof FlowRootNode)) { + return false; + } + LocationFileGroupNode other = (LocationFileGroupNode) obj; + return Objects.equals(filePath, other.filePath) && Objects.equals(groupIndex, other.groupIndex); + } + + } + private static class RootNode { private final IMarker rootMarker; - private final List flowsMarkers; + private final MarkerFlows flows; - public RootNode(IMarker rootMarker, List flowsMarkers) { + public RootNode(IMarker rootMarker, MarkerFlows flows) { this.rootMarker = rootMarker; - this.flowsMarkers = flowsMarkers; + this.flows = flows; } public IMarker getMarker() { return rootMarker; } - public List getFlows() { - return flowsMarkers; + public MarkerFlows getFlows() { + return flows; } } @@ -178,7 +250,7 @@ private static class LocationsProvider implements ITreeContentProvider { @Override public Object[] getElements(Object inputElement) { IMarker sonarlintMarker = (IMarker) inputElement; - List flowsMarkers = MarkerUtils.getIssueFlows(sonarlintMarker); + MarkerFlows flowsMarkers = MarkerUtils.getIssueFlows(sonarlintMarker); if (!flowsMarkers.isEmpty()) { return new Object[] {new RootNode(sonarlintMarker, flowsMarkers)}; } else { @@ -189,22 +261,24 @@ public Object[] getElements(Object inputElement) { @Override public Object[] getChildren(Object parentElement) { if (parentElement instanceof RootNode) { - List flows = ((RootNode) parentElement).getFlows(); - if (flows.size() > 1) { + MarkerFlows flows = ((RootNode) parentElement).getFlows(); + if (flows.count() > 1) { // Flatten if all flows have a single location - if (flows.stream().allMatch(f -> f.getLocations().size() <= 1)) { - return flows.stream().map(FlowRootNode::new).flatMap(f -> Stream.of(f.getChildren())).toArray(); + if (flows.isSecondaryLocations()) { + return flows.getFlows().stream().map(FlowRootNode::new).flatMap(f -> Stream.of(f.getChildren())).toArray(); } else { - return flows.stream().map(FlowRootNode::new).toArray(); + return flows.getFlows().stream().map(FlowRootNode::new).toArray(); } - } else if (flows.size() == 1) { + } else if (flows.count() == 1) { // Don't show flow number - return new FlowRootNode(flows.get(0)).getChildren(); + return new FlowRootNode(flows.getFlows().get(0)).getChildren(); } else { return new Object[0]; } } else if (parentElement instanceof FlowRootNode) { return ((FlowRootNode) parentElement).getChildren(); + } else if (parentElement instanceof LocationFileGroupNode) { + return ((LocationFileGroupNode) parentElement).getChildren().toArray(); } else { return new Object[0]; } @@ -247,8 +321,10 @@ private static String getText(Object element) { 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 FlowLocationNode) { + return ((FlowLocationNode) element).getLabel(); + } else if (element instanceof LocationFileGroupNode) { + return Paths.get(((LocationFileGroupNode) element).getFilePath()).getFileName().toString(); } else if (element instanceof String) { return (String) element; } @@ -276,23 +352,21 @@ private StyledString getStyledString(Object element) { return new StyledString(getText(element), isValidLocation(element) ? null : invalidLocationStyler); } - } - - private static boolean isValidLocation(Object element) { - if (element instanceof RootNode) { - return ((RootNode) element).getMarker().exists(); - } else if (element instanceof FlowRootNode) { - return Stream.of(((FlowRootNode) element).getChildren()).anyMatch(IssueLocationsView::isValidFlowLocation); - } else if (element instanceof FlowNode) { - return isValidFlowLocation(((FlowNode) element)); - } else if (element instanceof String) { - return true; - } - throw new IllegalArgumentException("Unknow node type: " + element); - } + private static boolean isValidLocation(Object element) { + if (element instanceof RootNode) { + return ((RootNode) element).getMarker().exists(); + } else if (element instanceof FlowRootNode) { + return Stream.of(((FlowRootNode) element).getChildren()).anyMatch(LocationNode::isValid); + } else if (element instanceof FlowLocationNode) { + return ((FlowLocationNode) element).isValid(); + } else if (element instanceof LocationFileGroupNode) { + return ((LocationFileGroupNode) element).isValid(); + } else if (element instanceof String) { + return true; + } + throw new IllegalArgumentException("Unknown node type: " + element); + } - private static boolean isValidFlowLocation(FlowNode flowNode) { - return flowNode.getLocation().getMarker().exists() && !flowNode.getLocation().isDeleted(); } @Override @@ -353,8 +427,10 @@ private static void onTreeNodeSelected(Object selectedNode) { SonarLintUiPlugin.getSonarlintMarkerSelectionService().flowSelected(null); } else if (selectedNode instanceof FlowRootNode) { SonarLintUiPlugin.getSonarlintMarkerSelectionService().flowSelected(((FlowRootNode) selectedNode).getFlow()); - } else if (selectedNode instanceof FlowNode) { - SonarLintUiPlugin.getSonarlintMarkerSelectionService().flowLocationSelected(((FlowNode) selectedNode).getLocation()); + } else if (selectedNode instanceof FlowLocationNode) { + SonarLintUiPlugin.getSonarlintMarkerSelectionService().flowLocationSelected(((FlowLocationNode) selectedNode).getLocation()); + } else if (selectedNode instanceof LocationFileGroupNode) { + SonarLintUiPlugin.getSonarlintMarkerSelectionService().flowLocationSelected(((LocationFileGroupNode) selectedNode).getChildren().get(0).getLocation()); } else { throw new IllegalStateException("Unsupported node type"); } @@ -362,9 +438,15 @@ private static void onTreeNodeSelected(Object selectedNode) { private static void onTreeNodeDoubleClick(Object node) { IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); - if (node instanceof FlowNode) { - IMarker flowMarker = ((FlowNode) node).getLocation().getMarker(); - if (flowMarker.exists()) { + MarkerFlowLocation location = null; + if (node instanceof FlowLocationNode) { + location = ((FlowLocationNode) node).getLocation(); + } else if (node instanceof LocationFileGroupNode) { + location = ((LocationFileGroupNode) node).getChildren().get(0).getLocation(); + } + if (location != null) { + IMarker flowMarker = location.getMarker(); + if (flowMarker != null && flowMarker.exists()) { try { IDE.openEditor(page, flowMarker); } catch (PartInitException e) { @@ -423,7 +505,7 @@ public void setShowAnnotations(boolean b) { } public void selectLocation(MarkerFlowLocation location) { - locationsViewer.setSelection(new StructuredSelection(new FlowNode(location)), true); + locationsViewer.setSelection(new StructuredSelection(new FlowLocationNode(location)), true); } public void selectFlow(MarkerFlow flow) { @@ -431,7 +513,7 @@ public void selectFlow(MarkerFlow flow) { } public void refreshLabel(MarkerFlowLocation location) { - locationsViewer.refresh(new FlowNode(location)); + locationsViewer.refresh(new FlowLocationNode(location)); } }