Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: select treenode on snyk showdocument [IDE-957] #260

Merged
merged 21 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -125,4 +127,6 @@ static String getPlural(long count) {
* @return
*/
abstract void disableDelta();

abstract void selectTreeNode(Issue issue, String product);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -384,16 +392,111 @@ public void snykScan(SnykScanParam param) {
this.toolView.refreshBrowser(param.getStatus());
}

@JsonNotification(value = LsConstants.SNYK_FOLDER_CONFIG)
public void folderConfig(FolderConfigsParam folderConfigParam) {
List<FolderConfig> 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<FolderConfig> folderConfigs = folderConfigParam != null ? folderConfigParam.getFolderConfigs() : List.of();
CompletableFuture.runAsync(() -> FolderConfigs.getInstance().addAll(folderConfigs));
@Override
public CompletableFuture<ShowDocumentResult> 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 ("snyk".equals(scheme) && product.equals(DIAGNOSTIC_SOURCE_SNYK_CODE)
&& "showInDetailPanel".equals(action)) {
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this to a utility class? I don't think it should be public otherwise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String query = uri.getQuery();
if (query == null || query.isEmpty()) {
return null;
}

Map<String, String> paramMap = parseQueryString(query);
String value = paramMap.get(paramName);

if (value == null) {
return null;
}

try {
return URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
SnykLogger.logError(e);
return null;
}
}

public static Map<String, String> parseQueryString(String queryString) {
Map<String, String> paramMap = new HashMap<>();

if (queryString == null || queryString.isEmpty()) {
return paramMap;
}

int keyValueSize = 2;
for (String param : queryString.split("&")) {
if (!param.isEmpty()) {
String[] keyValue = param.split("=");

if (keyValue.length == keyValueSize) {
try {
paramMap.put(keyValue[0], URLDecoder.decode(keyValue[1], "UTF-8"));
} catch (UnsupportedEncodingException e) {
SnykLogger.logError(e);
}
}
}
}

return paramMap;
}

private void openToolView() {
Expand Down Expand Up @@ -473,7 +576,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");
Expand Down Expand Up @@ -548,11 +651,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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import static io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView.CONGRATS_NO_ISSUES_FOUND;
import static io.snyk.eclipse.plugin.views.snyktoolview.ISnykToolView.getPlural;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
Expand All @@ -24,6 +25,7 @@
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
Expand Down Expand Up @@ -187,12 +189,11 @@ void testPublishDiagnosticsShouldChangeCache() {
var folderPath = "/a/b";
var uri = "file://" + folderPath + "/c/";

if(SystemUtils.IS_OS_WINDOWS) {
if (SystemUtils.IS_OS_WINDOWS) {
folderPath = "C://a/b";
uri = "file:///" + folderPath + "/c/";
}


File pathFromUri = LSPEclipseUtils.fromUri(URI.create(uri));
var filePath = pathFromUri.getAbsolutePath();
var issueCache = new SnykIssueCache(Paths.get(folderPath));
Expand Down Expand Up @@ -584,4 +585,40 @@ void testGetTotal() throws IOException {
private AdditionalData getSecurityIssue() {
return Instancio.of(AdditionalData.class).set(Select.field(AdditionalData::isSecurityType), true).create();
}

@Test
public void getDecodedParam_Returns_Parameter_Value_If_Present_In_Query_String() throws Exception {
cut = new SnykExtendedLanguageClient();
String query = "product=Snyk+Code&issueId=7642f506c568056a7090d3ceb7b3c2e0&action=showInDetailPanel";
URI uriWithQuery = new URI("snyk://path/to/resource?" + query);

var result = cut.getDecodedParam(uriWithQuery, "issueId");
assertEquals("7642f506c568056a7090d3ceb7b3c2e0", result);

result = cut.getDecodedParam(uriWithQuery, "action");
assertEquals("showInDetailPanel", result);

result = cut.getDecodedParam(uriWithQuery, "product");
assertEquals("Snyk Code", result);
}

@Test
public void parseQueryString_Returns_Parameters_In_Query_String() throws Exception {
cut = new SnykExtendedLanguageClient();
String query = "product=Snyk+Code&issueId=7642f506c568056a7090d3ceb7b3c2e0&action=showInDetailPanel";
URI uriWithQuery = new URI("snyk://path/to/resource?" + query);

var result = cut.parseQueryString(uriWithQuery.getQuery());
assertEquals(3, result.size());
}

@Test
public void parseQueryString_Returns_Empty_Map_If_Query_String_Is_Empty() throws URISyntaxException {
cut = new SnykExtendedLanguageClient();
URI uriWithoutQuery = new URI("snyk://path/to/resource");

var result = cut.parseQueryString(null);
assertTrue(result.isEmpty());
}

}