From 269c2ae429b3867986f1beca4f6b259c220fc394 Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Mon, 24 Feb 2025 21:58:25 +0100 Subject: [PATCH] feat: select treenode on snyk showdocument WIP --- .../views/snyktoolview/ISnykToolView.java | 4 + .../views/snyktoolview/SnykToolView.java | 29 +++++ .../io/snyk/languageserver/LsConstants.java | 1 + .../SnykExtendedLanguageClient.java | 108 ++++++++++++++++-- 4 files changed, 132 insertions(+), 10 deletions(-) diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/ISnykToolView.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/ISnykToolView.java index 5096a28f..87ebfd24 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/ISnykToolView.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/ISnykToolView.java @@ -2,6 +2,8 @@ import org.eclipse.jface.viewers.TreeViewer; +import io.snyk.languageserver.protocolextension.messageObjects.scanResults.Issue; + /** * This interface captures the externally used methods with the tool window. * Having it, should allow for easier testing of the business logic apart from @@ -125,4 +127,6 @@ static String getPlural(long count) { * @return */ abstract void disableDelta(); + + abstract void selectTreeNode(Issue issue, String product); } diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java index 364682a6..90dfdcdc 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/SnykToolView.java @@ -21,6 +21,7 @@ import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeNode; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.lsp4e.LSPEclipseUtils; @@ -42,6 +43,7 @@ import org.eclipse.ui.menus.CommandContributionItemParameter; import org.eclipse.ui.part.ViewPart; +import io.snyk.eclipse.plugin.domain.ProductConstants; import io.snyk.eclipse.plugin.preferences.Preferences; import io.snyk.eclipse.plugin.properties.FolderConfigs; import io.snyk.eclipse.plugin.utils.ResourceUtils; @@ -477,4 +479,31 @@ protected void outputCommandResult(Object result) { } } + @Override + public void selectTreeNode(Issue issue, String product) { + ProductTreeNode productNode = getProductNode(ProductConstants.DISPLAYED_CODE_SECURITY, issue.filePath()); + drillDown((TreeNode) productNode, issue); + } + + private void drillDown(TreeNode currentParent, Issue issue) { + for (Object child : currentParent.getChildren()) { + TreeNode childNode = (TreeNode) child; + + if (childNode instanceof IssueTreeNode && ((IssueTreeNode) childNode).getIssue().id().equals(issue.id())) { + updateSelection((IssueTreeNode) childNode); + return; // Exit the function as we've found a match + } + + if (childNode.getChildren() != null && childNode.getChildren().length != 0) { + drillDown(childNode, issue); + } + } + } + + private void updateSelection(IssueTreeNode issueTreeNode) { + Display.getDefault().asyncExec(() -> { + IStructuredSelection selection = new StructuredSelection(issueTreeNode); + treeViewer.setSelection(selection, true); + }); + } } diff --git a/plugin/src/main/java/io/snyk/languageserver/LsConstants.java b/plugin/src/main/java/io/snyk/languageserver/LsConstants.java index 243d8b18..a57213fa 100644 --- a/plugin/src/main/java/io/snyk/languageserver/LsConstants.java +++ b/plugin/src/main/java/io/snyk/languageserver/LsConstants.java @@ -26,4 +26,5 @@ private LsConstants() { public static final String SNYK_PUBLISH_DIAGNOSTICS_316 = "$/snyk.publishDiagnostics316"; public static final String SNYK_FOLDER_CONFIG = "$/snyk.folderConfigs"; public static final String SNYK_SCAN_SUMMARY = "$/snyk.scanSummary"; + public static final String SNYK_SHOW_DOCUMENT = "$/snyk.showDocument"; } \ No newline at end of file diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java index c2a054a9..3f228d9a 100644 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java @@ -1,5 +1,6 @@ package io.snyk.languageserver.protocolextension; +import static io.snyk.eclipse.plugin.domain.ProductConstants.DIAGNOSTIC_SOURCE_SNYK_CODE; import static io.snyk.eclipse.plugin.domain.ProductConstants.DISPLAYED_CODE_QUALITY; import static io.snyk.eclipse.plugin.domain.ProductConstants.DISPLAYED_CODE_SECURITY; import static io.snyk.eclipse.plugin.domain.ProductConstants.LSP_SOURCE_TO_SCAN_PARAMS; @@ -22,15 +23,20 @@ import static io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView.getPlural; import java.io.File; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; @@ -47,6 +53,8 @@ import org.eclipse.lsp4e.LSPEclipseUtils; import org.eclipse.lsp4e.LanguageClientImpl; import org.eclipse.lsp4j.ProgressParams; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; import org.eclipse.lsp4j.WorkDoneProgressCreateParams; import org.eclipse.lsp4j.WorkDoneProgressKind; import org.eclipse.lsp4j.WorkDoneProgressNotification; @@ -116,8 +124,8 @@ public class SnykExtendedLanguageClient extends LanguageClientImpl { public SnykExtendedLanguageClient() { super(); - //TODO, fix this; Identifies a possible unsafe usage of a static field. - instance = this; //NOPMD + // TODO, fix this; Identifies a possible unsafe usage of a static field. + instance = this; // NOPMD om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); registerPluginInstalledEventTask(); registerRefreshFeatureFlagsTask(); @@ -384,16 +392,96 @@ public void snykScan(SnykScanParam param) { this.toolView.refreshBrowser(param.getStatus()); } + @JsonNotification(value = LsConstants.SNYK_FOLDER_CONFIG) + public void folderConfig(FolderConfigsParam folderConfigParam) { + List folderConfigs = folderConfigParam != null ? folderConfigParam.getFolderConfigs() : List.of(); + CompletableFuture.runAsync(() -> FolderConfigs.getInstance().addAll(folderConfigs)); + } + @JsonNotification(value = LsConstants.SNYK_SCAN_SUMMARY) public void updateSummaryPanel(SummaryPanelParams summary) { openToolView(); this.toolView.updateSummary(summary.getSummary()); } - @JsonNotification(value = LsConstants.SNYK_FOLDER_CONFIG) - public void folderConfig(FolderConfigsParam folderConfigParam) { - List folderConfigs = folderConfigParam != null ? folderConfigParam.getFolderConfigs() : List.of(); - CompletableFuture.runAsync(() -> FolderConfigs.getInstance().addAll(folderConfigs)); + @Override + public CompletableFuture showDocument(ShowDocumentParams params) { + URI uri; + try { + uri = new URI(params.getUri()); + } catch (URISyntaxException e) { + SnykLogger.logError(e); + return null; + } + + String scheme = uri.getScheme(); + String product = getDecodedParam(uri, "product"); + String action = getDecodedParam(uri, "action"); + String issueId = getDecodedParam(uri, "issueId"); + + if (!"snyk".equals(scheme)) { + SnykLogger.logInfo(String.format("Invalid URI: expected 'snyk' scheme but got %s", scheme)); + } else if (!DIAGNOSTIC_SOURCE_SNYK_CODE.equals(product)) { + SnykLogger.logInfo(String.format("Invalid URI: expected '{}' product but got %s", + DIAGNOSTIC_SOURCE_SNYK_CODE, product)); + } else if (!"showInDetailPanel".equals(action)) { + SnykLogger.logInfo(String.format("Invalid URI: expected 'showInDetailPanel' action but got %s", action)); + } else if (issueId.isEmpty()) { + SnykLogger.logInfo(String.format("Invalid URI: 'issueId' empty")); + } + + if (scheme.equals("snyk") && product.equals(DIAGNOSTIC_SOURCE_SNYK_CODE) + && action.equals("showInDetailPanel")) { + return CompletableFuture.supplyAsync(() -> { + + Issue issue = getIssueFromCache(uri); + this.toolView.selectTreeNode(issue, product); + return new ShowDocumentResult(true); + }); + } else { + SnykLogger.logInfo(String.format("Invalid URI: scheme=%s, product=%s, action=%s", scheme, product, action)); + return super.showDocument(params); + } + } + + public Issue getIssueFromCache(URI uri) { + String filePath = uri.getPath(); + String issueId = getDecodedParam(uri, "issueId"); + + SnykIssueCache issueCache = getIssueCache(filePath); + return issueCache.getCodeSecurityIssuesForPath(filePath).stream().filter(i -> i.id().equals(issueId)) + .findFirst().orElse(null); + } + + public String getDecodedParam(URI uri, String paramName) { + Map paramMap = parseQueryString(uri.getQuery()); + + try { + return URLDecoder.decode(paramMap.get(paramName), "UTF-8"); + } catch (UnsupportedEncodingException e) { + SnykLogger.logError(e); + } + return null; + } + + private static Map parseQueryString(String queryString) { + Map paramMap = new HashMap<>(); + + for (String param : queryString.split("&")) { + if (!param.isEmpty()) { + String[] keyValue = param.split("="); + + if (keyValue.length == 2) { + try { + paramMap.put(keyValue[0], URLDecoder.decode(keyValue[1], "UTF-8")); + } catch (UnsupportedEncodingException e) { + SnykLogger.logError(e); + } + } + } + } + + return paramMap; } private void openToolView() { @@ -473,7 +561,7 @@ public String getCountsSuffix(ProductTreeNode productTreeNode, SnykIssueCache is high, medium, low); } - private SnykIssueCache getIssueCache(String filePath) { + public SnykIssueCache getIssueCache(String filePath) { var issueCache = IssueCacheHolder.getInstance().getCacheInstance(Paths.get(filePath)); if (issueCache == null) { throw new IllegalArgumentException("No issue cache for param possible"); @@ -548,11 +636,11 @@ private void populateFileAndIssueNodes(ProductTreeNode productTreeNode, SnykIssu if (issuesList.isEmpty()) continue; - - FileTreeNode fileNode = new FileTreeNode(fileName); //NOPMD + + FileTreeNode fileNode = new FileTreeNode(fileName); // NOPMD toolView.addFileNode(productTreeNode, fileNode); for (Issue issue : issuesList) { - toolView.addIssueNode(fileNode, new IssueTreeNode(issue)); //NOPMD + toolView.addIssueNode(fileNode, new IssueTreeNode(issue)); // NOPMD } } }