diff --git a/CHANGELOG.md b/CHANGELOG.md index 075b7e08..ebfd00c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changes - add option for using a folder as reference instead of a branch in net-new scanning - add scan summary to custom UI, updating scan statuses live +- added support for DeepCode AI Fixes ## [3.0.0] ### Changes diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java index a587ce41..8cb14dcf 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/BaseHtmlProvider.java @@ -6,7 +6,6 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.jface.resource.ColorRegistry; -import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; @@ -120,9 +119,10 @@ public String replaceCssVariables(String html) { getColorAsHex("org.eclipse.ui.workbench.INACTIVE_TAB_BG_START", "#F0F0F0")); htmlStyled = htmlStyled.replace("var(--circle-color)", getColorAsHex("org.eclipse.ui.workbench.INACTIVE_TAB_BG_START", "#F0F0F0")); - htmlStyled = htmlStyled.replace("var(--border-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_OUTER_KEYLINE_COLOR", "#CCCCCC")); + htmlStyled = htmlStyled.replace("var(--input-border)", + getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_OUTER_KEYLINE_COLOR", "#CCCCCC")); htmlStyled = htmlStyled.replace("var(--link-color)", getColorAsHex("ACTIVE_HYPERLINK_COLOR", "#0000FF")); htmlStyled = htmlStyled.replace("var(--horizontal-border-color)", getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_OUTER_KEYLINE_COLOR", "#CCCCCC")); @@ -142,8 +142,10 @@ private String getScaledFontSize() { } catch (IllegalStateException e) { defaultHeight = 13; } - // Language server HTML assumes a base font size of 10px. The default Eclipse font size is 17px (13pt), so we - // apply a scaling factor here. This ensures that HTML fonts scale correctly if the user changes the text size. + // Language server HTML assumes a base font size of 10px. The default Eclipse + // font size is 17px (13pt), so we + // apply a scaling factor here. This ensures that HTML fonts scale correctly if + // the user changes the text size. int scaledHeight = (int) (defaultHeight / 1.7); return scaledHeight + "pt"; } diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java index 5f18a55a..bac2a28a 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/html/CodeHtmlProvider.java @@ -3,7 +3,7 @@ import io.snyk.eclipse.plugin.preferences.Preferences; public class CodeHtmlProvider extends BaseHtmlProvider { - private static CodeHtmlProvider instance = new CodeHtmlProvider(); + private static CodeHtmlProvider instance = new CodeHtmlProvider(); public static CodeHtmlProvider getInstance() { synchronized (CodeHtmlProvider.class) { @@ -16,61 +16,80 @@ public static CodeHtmlProvider getInstance() { return instance; } - @Override - public String getInitScript() { - String themeScript = getThemeScript(); - String initScript = super.getInitScript(); - return initScript + "\n" + """ - function navigateToIssue(e, target) { - e.preventDefault(); - var filePath = target.getAttribute('file-path'); - var startLine = target.getAttribute('start-line'); - var endLine = target.getAttribute('end-line'); - var startCharacter = target.getAttribute('start-character'); - var endCharacter = target.getAttribute('end-character'); - window.openInEditor(filePath, startLine, endLine, startCharacter, endCharacter); - } - var navigatableLines = document.getElementsByClassName('data-flow-clickable-row'); - for(var i = 0; i < navigatableLines.length; i++) { - navigatableLines[i].onclick = function(e) { - navigateToIssue(e, this); - return false; - }; - } - if(document.getElementById('position-line')) { - document.getElementById('position-line').onclick = function(e) { - var target = navigatableLines[0]; - if(target) { - navigateToIssue(e, target); - } - } - } - // Disable AIfix - if(document.getElementById('ai-fix-wrapper') && document.getElementById('no-ai-fix-wrapper')){ - document.getElementById('ai-fix-wrapper').className = 'hidden'; - document.getElementById('no-ai-fix-wrapper').className = ''; - } - """ + themeScript; - } + @Override + public String getInitScript() { + String themeScript = getThemeScript(); + String initScript = super.getInitScript(); + return initScript + "\n" + """ + function navigateToIssue(e, target) { + e.preventDefault(); + var filePath = target.getAttribute('file-path'); + var startLine = target.getAttribute('start-line'); + var endLine = target.getAttribute('end-line'); + var startCharacter = target.getAttribute('start-character'); + var endCharacter = target.getAttribute('end-character'); + window.openInEditor(filePath, startLine, endLine, startCharacter, endCharacter); + } + var navigatableLines = document.getElementsByClassName('data-flow-clickable-row'); + for(var i = 0; i < navigatableLines.length; i++) { + navigatableLines[i].onclick = function(e) { + navigateToIssue(e, this); + return false; + }; + } + if(document.getElementById('position-line')) { + document.getElementById('position-line').onclick = function(e) { + var target = navigatableLines[0]; + if(target) { + navigateToIssue(e, target); + } + } + } + """ + themeScript; + } + + private String getThemeScript() { + if (Preferences.getInstance().isTest()) { + return ""; + } + + String themeScript = "var isDarkTheme = " + isDarkTheme() + ";\n" + + "document.body.classList.add(isDarkTheme ? 'dark' : 'light');"; + return themeScript; + } + + @Override + public String replaceCssVariables(String html) { + String htmlStyled = super.replaceCssVariables(html); + + // Replace CSS variables with actual color values + htmlStyled = htmlStyled.replace("var(--example-line-removed-color)", + super.getColorAsHex("DELETION_COLOR", "#ff0000")); + htmlStyled = htmlStyled.replace("var(--example-line-added-color)", + super.getColorAsHex("ADDITION_COLOR", "#00ff00")); + htmlStyled = htmlStyled.replace("var(--generated-ai-fix-button-background-color)", + super.getColorAsHex("BUTTON_COLOR", "#375578")); + htmlStyled = htmlStyled.replace("var(--disabled-background-color)", + super.getColorAsHex("org.eclipse.ui.workbench.ACTIVE_TAB_OUTER_KEYLINE_COLOR", "#CCCCCC")); - private String getThemeScript() { - if(Preferences.getInstance().isTest()) { - return ""; - } + String htmlWithScripts = replaceAIFixScripts(htmlStyled); - String themeScript = "var isDarkTheme = " + isDarkTheme() + ";\n" + - "document.body.classList.add(isDarkTheme ? 'dark' : 'light');"; - return themeScript; - } + return htmlWithScripts; + } + + private String replaceAIFixScripts(String html) { + String htmlWithAiFixScripts = html.replace("${ideGenerateAIFix}", getGenerateAiFixScript()); + htmlWithAiFixScripts = htmlWithAiFixScripts.replace("${ideApplyAIFix}", getApplyAiFixScript()); + + return htmlWithAiFixScripts; + } - @Override - public String replaceCssVariables(String html) { - String htmlStyled = super.replaceCssVariables(html); - - // Replace CSS variables with actual color values - htmlStyled = htmlStyled.replace("var(--example-line-removed-color)", super.getColorAsHex("DELETION_COLOR", "#ff0000")); - htmlStyled = htmlStyled.replace("var(--example-line-added-color)", super.getColorAsHex("ADDITION_COLOR", "#00ff00")); + private String getGenerateAiFixScript() { + return "window.ideGenAIFix(generateFixQueryString)\n;"; + } + + private String getApplyAiFixScript() { + return "window.ideApplyFix(fixId);\n"; + } - return htmlStyled; - } } diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/utils/UriUtils.java b/plugin/src/main/java/io/snyk/eclipse/plugin/utils/UriUtils.java new file mode 100644 index 00000000..86a0c5b0 --- /dev/null +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/utils/UriUtils.java @@ -0,0 +1,78 @@ +package io.snyk.eclipse.plugin.utils; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class UriUtils { + + /** + * Retrieves a parameter value from the given URI. + * + * If the query string does not contain the specified parameter or is + * null/empty, this method returns null. + * + * @param uri + * The URI to retrieve parameters from + * @param paramName + * The name of the parameter to retrieve + * @return The parameter value if it exists in the query string, otherwise + * null + */ + public static String getDecodedParam(URI uri, String paramName) { + String query = uri.getQuery(); + if (query == null || query.isEmpty()) { + return null; + } + + Map paramMap = getQueryParameters(query); + String value = paramMap.get(paramName); + + if (value == null) { + return null; + } + + return value; + } + + /** + * Parses a query string into a map of key-value pairs, decoding any + * URL-encoded values. + * + * If the input is null or empty, an empty map is returned. If an encoding + * error occurs while decoding a value, an error is logged and an empty map + * is returned. + * + * @param queryString + * The query string to parse + * @return A map containing the decoded parameters + */ + public static Map getQueryParameters(String queryString) { + Map paramMap = new HashMap<>(); + + if (queryString == null || queryString.isEmpty()) { + return paramMap; + } + + for (String param : queryString.split("&")) { + if (!param.isEmpty()) { + String[] keyValue = param.split("="); + + if (keyValue.length == 2) { // NOPMD, AvoidLiteralsInIfCondition + try { + paramMap.put(keyValue[0], + URLDecoder.decode(keyValue[1], "UTF-8")); + } catch (UnsupportedEncodingException e) { + SnykLogger.logError(e); + return Collections.emptyMap(); + } + } + } + } + + return paramMap; + } +} \ No newline at end of file diff --git a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java index 5413af0e..8ba977f6 100644 --- a/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java +++ b/plugin/src/main/java/io/snyk/eclipse/plugin/views/snyktoolview/BrowserHandler.java @@ -3,6 +3,7 @@ import static org.apache.commons.lang3.StringUtils.isEmpty; import java.nio.file.Paths; +import java.util.Collections; import java.util.concurrent.CompletableFuture; import org.eclipse.core.commands.common.CommandException; @@ -31,6 +32,7 @@ import io.snyk.eclipse.plugin.utils.SnykLogger; import io.snyk.eclipse.plugin.views.snyktoolview.handlers.IHandlerCommands; import io.snyk.eclipse.plugin.wizards.SnykWizard; +import io.snyk.languageserver.protocolextension.SnykExtendedLanguageClient; @SuppressWarnings("restriction") public class BrowserHandler { @@ -84,17 +86,44 @@ public Object function(Object[] arguments) { new BrowserFunction(browser, "stopScan") { @Override public Object function(Object[] arguments) { - IHandlerService handlerService = - (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class); + IHandlerService handlerService = (IHandlerService) PlatformUI.getWorkbench() + .getService(IHandlerService.class); + try { handlerService.executeCommand(IHandlerCommands.STOP_SCAN, null); } catch (CommandException e) { SnykLogger.logError(e); - } + } return null; } }; + new BrowserFunction(browser, "ideGenAIFix") { + @Override + public Object function(Object[] arguments) { + String params = (String) arguments[0]; + String[] parts = params.split("@|@"); + String folderURI = (String) parts[0]; + String fileURI = (String) parts[2]; + String issueID = (String) parts[4]; + + SnykExtendedLanguageClient.getInstance().sendCodeFixDiffsCommand(folderURI, fileURI, issueID); + + return Collections.emptyList(); + } + }; + + new BrowserFunction(browser, "ideApplyFix") { + @Override + public Object function(Object[] arguments) { + String fixId = (String) arguments[0]; + + SnykExtendedLanguageClient.getInstance().sendCodeApplyAiFixEditCommand(fixId); + + return Collections.emptyList(); + } + }; + browser.addLocationListener(new LocationListener() { @Override public void changing(LocationEvent event) { @@ -158,7 +187,7 @@ public CompletableFuture updateBrowserContent(TreeNode node) { } final var browserContent = htmlProvider.replaceCssVariables(htmlContent); - + Display.getDefault().syncExec(() -> { browser.setText(browserContent); }); @@ -191,7 +220,8 @@ public String generateHtmlContent(String text) { } public void setDefaultBrowserText() { - // If we are not authenticated, show the welcome page, else show the issue placeholder. + // If we are not authenticated, show the welcome page, else show the issue + // placeholder. if (Preferences.getInstance().getAuthToken().isBlank()) { browser.setText(StaticPageHtmlProvider.getInstance().getInitHtml()); } else { 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..11fc02a5 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(product, issue.filePath()); + selectTreenodeForIssue((TreeNode) productNode, issue); + } + + private void selectTreenodeForIssue(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) { + selectTreenodeForIssue(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..d9fd926d 100644 --- a/plugin/src/main/java/io/snyk/languageserver/LsConstants.java +++ b/plugin/src/main/java/io/snyk/languageserver/LsConstants.java @@ -17,6 +17,7 @@ private LsConstants() { public static final String COMMAND_REPORT_ANALYTICS = "snyk.reportAnalytics"; public static final String COMMAND_GET_FEATURE_FLAG_STATUS = "snyk.getFeatureFlagStatus"; public static final String COMMAND_CODE_FIX_DIFFS = "snyk.code.fixDiffs"; + public static final String COMMAND_CODE_FIX_APPLY_AI_EDIT = "snyk.code.fixApplyEdit"; public static final String COMMAND_CODE_SUBMIT_FIX_FEEDBACK = "snyk.code.submitFixFeedback"; public static final String COMMAND_SNYK_CLI = "snyk.executeCLI"; public static final String SNYK_HAS_AUTHENTICATED = "$/snyk.hasAuthenticated"; @@ -26,4 +27,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..8480c014 100644 --- a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClient.java @@ -23,6 +23,7 @@ import java.io.File; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; @@ -47,6 +48,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,15 +119,16 @@ 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(); } public static SnykExtendedLanguageClient getInstance() { - return instance; // we leave instantiation to LSP4e, no lazy construction here + return instance; // we leave instantiation to LSP4e, no lazy + // construction here } public LanguageServer getConnectedLanguageServer() { @@ -143,20 +147,23 @@ public void updateConfiguration() { @Override public CompletableFuture> workspaceFolders() { - return CompletableFuture.completedFuture(ResourceUtils.getAccessibleTopLevelProjects().stream() - .map(LSPEclipseUtils::toWorkspaceFolder).toList()); + return CompletableFuture.completedFuture( + ResourceUtils.getAccessibleTopLevelProjects().stream() + .map(LSPEclipseUtils::toWorkspaceFolder).toList()); } private void registerRefreshFeatureFlagsTask() { if (taskProcessor == null) { taskProcessor = TaskProcessor.getInstance(); } - Consumer refreshFeatureFlagsConsumer = lc -> lc.refreshFeatureFlags(); + Consumer refreshFeatureFlagsConsumer = lc -> lc + .refreshFeatureFlags(); taskProcessor.registerTask(refreshFeatureFlagsConsumer, null); } public void refreshFeatureFlags() { - boolean enableConsistentIgnores = getFeatureFlagStatus(FeatureFlagConstants.SNYK_CODE_CONSISTENT_IGNORES); + boolean enableConsistentIgnores = getFeatureFlagStatus( + FeatureFlagConstants.SNYK_CODE_CONSISTENT_IGNORES); toggleIgnores(enableConsistentIgnores); } @@ -164,7 +171,8 @@ private void toggleIgnores(Boolean enableConsistentIgnores) { if (Preferences.getInstance().isTest()) { return; } - Preferences.getInstance().store(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED, + Preferences.getInstance().store( + Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED, Boolean.valueOf(enableConsistentIgnores).toString()); PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { var snykToolView = SnykStartup.getView(); @@ -174,14 +182,18 @@ private void toggleIgnores(Boolean enableConsistentIgnores) { } private void registerPluginInstalledEventTask() { - if (!Preferences.getInstance().getBooleanPref(Preferences.ANALYTICS_PLUGIN_INSTALLED_SENT, false)) { + if (!Preferences.getInstance().getBooleanPref( + Preferences.ANALYTICS_PLUGIN_INSTALLED_SENT, false)) { if (taskProcessor == null) { taskProcessor = TaskProcessor.getInstance(); } - var pluginInstalledEvent = new AnalyticsEventTask("plugin installed", List.of("install")); - Consumer analyticsTask = lc -> lc.reportAnalytics(pluginInstalledEvent); + var pluginInstalledEvent = new AnalyticsEventTask( + "plugin installed", List.of("install")); + Consumer analyticsTask = lc -> lc + .reportAnalytics(pluginInstalledEvent); Consumer analyticsCallback = v -> { - Preferences.getInstance().store(Preferences.ANALYTICS_PLUGIN_INSTALLED_SENT, "true"); + Preferences.getInstance().store( + Preferences.ANALYTICS_PLUGIN_INSTALLED_SENT, "true"); }; taskProcessor.registerTask(analyticsTask, analyticsCallback); @@ -196,11 +208,14 @@ public void triggerScan(Path projectPath) { openToolView(); try { if (projectPath != null) { - executeCommand(LsConstants.COMMAND_WORKSPACE_FOLDER_SCAN, List.of(projectPath)); + executeCommand( + LsConstants.COMMAND_WORKSPACE_FOLDER_SCAN, + List.of(projectPath)); return; } - executeCommand(LsConstants.COMMAND_WORKSPACE_SCAN, new ArrayList<>()); + executeCommand(LsConstants.COMMAND_WORKSPACE_SCAN, + new ArrayList<>()); } catch (Exception e) { SnykLogger.logError(e); @@ -209,7 +224,8 @@ public void triggerScan(Path projectPath) { }); } - private CompletableFuture executeCommand(String cmd, List args) { + private CompletableFuture executeCommand(String cmd, + List args) { synchronized (chSyncObject) { if (commandHandler == null) { commandHandler = CommandHandler.getInstance(); @@ -227,7 +243,8 @@ public void ensureLanguageServerRunning() { try { Thread.sleep(1000); } catch (InterruptedException e) { - SnykLogger.logInfo("waiting for language server startup was interrupted"); + SnykLogger.logInfo( + "waiting for language server startup was interrupted"); Thread.currentThread().interrupt(); break; } @@ -241,21 +258,25 @@ public void ensureLanguageServerRunning() { } public void trustWorkspaceFolders() { - executeCommand(LsConstants.COMMAND_TRUST_WORKSPACE_FOLDERS, new ArrayList<>()); + executeCommand(LsConstants.COMMAND_TRUST_WORKSPACE_FOLDERS, + new ArrayList<>()); } public boolean getSastEnabled() { try { - CompletableFuture lsSastSettings = executeCommand(LsConstants.COMMAND_GET_SETTINGS_SAST_ENABLED, + CompletableFuture lsSastSettings = executeCommand( + LsConstants.COMMAND_GET_SETTINGS_SAST_ENABLED, new ArrayList<>()); Object result; try { result = lsSastSettings.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { - SnykLogger.logInfo("did not get a response for sast settings, disabling Snyk Code"); + SnykLogger.logInfo( + "did not get a response for sast settings, disabling Snyk Code"); return false; } - SastSettings sastSettings = om.convertValue(result, SastSettings.class); + SastSettings sastSettings = om.convertValue(result, + SastSettings.class); return sastSettings != null ? sastSettings.sastEnabled : false; } catch (Exception e) { SnykLogger.logError(e); @@ -267,15 +288,19 @@ public boolean getSastEnabled() { public boolean getFeatureFlagStatus(String featureFlag) { try { CompletableFuture lsGlobalIgnoresFeatureFlag = executeCommand( - LsConstants.COMMAND_GET_FEATURE_FLAG_STATUS, List.of(featureFlag)); + LsConstants.COMMAND_GET_FEATURE_FLAG_STATUS, + List.of(featureFlag)); Object result; try { result = lsGlobalIgnoresFeatureFlag.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { return false; } - FeatureFlagStatus featureFlagStatus = om.convertValue(result, FeatureFlagStatus.class); - return featureFlagStatus != null ? featureFlagStatus.getOk() : false; + FeatureFlagStatus featureFlagStatus = om.convertValue(result, + FeatureFlagStatus.class); + return featureFlagStatus != null + ? featureFlagStatus.getOk() + : false; } catch (Exception e) { SnykLogger.logError(e); } @@ -288,20 +313,29 @@ public String getIssueDescription(String issueId) { SnykLogger.logInfo("issueId is empty"); return ""; } - CompletableFuture issueDescription = executeCommand(LsConstants.COMMAND_GENERATE_ISSUE_DESCRIPTION, + CompletableFuture issueDescription = executeCommand( + LsConstants.COMMAND_GENERATE_ISSUE_DESCRIPTION, List.of(issueId)); Object result; try { result = issueDescription.get(5, TimeUnit.SECONDS); } catch (Exception ex) { - SnykLogger.logInfo( - "did not get issue description for issue " + issueId + "\n" + ExceptionUtils.getStackTrace(ex)); + SnykLogger.logInfo("did not get issue description for issue " + + issueId + "\n" + ExceptionUtils.getStackTrace(ex)); return ""; } return String.valueOf(result); } + public void sendCodeFixDiffsCommand(String folderURI, String fileURI, String issueID) { + executeCommand(LsConstants.COMMAND_CODE_FIX_DIFFS, List.of(folderURI, fileURI, issueID)); + } + + public void sendCodeApplyAiFixEditCommand(String fixId) { + executeCommand(LsConstants.COMMAND_CODE_FIX_APPLY_AI_EDIT, List.of(fixId)); + } + @JsonNotification(value = LsConstants.SNYK_HAS_AUTHENTICATED) public void hasAuthenticated(HasAuthenticatedParam param) { var prefs = Preferences.getInstance(); @@ -309,7 +343,8 @@ public void hasAuthenticated(HasAuthenticatedParam param) { var oldToken = prefs.getAuthToken(); var oldApi = prefs.getEndpoint(); - if (param.getApiUrl() != null && !param.getApiUrl().isBlank() && !param.getApiUrl().equals(oldApi)) { + if (param.getApiUrl() != null && !param.getApiUrl().isBlank() + && !param.getApiUrl().equals(oldApi)) { prefs.store(Preferences.ENDPOINT_KEY, param.getApiUrl()); } @@ -332,7 +367,8 @@ public void hasAuthenticated(HasAuthenticatedParam param) { @JsonNotification(value = LsConstants.SNYK_IS_AVAILABLE_CLI) public void isAvailableCli(SnykIsAvailableCliParams param) { - Preferences.getInstance().store(Preferences.CLI_PATH, param.getCliPath()); + Preferences.getInstance().store(Preferences.CLI_PATH, + param.getCliPath()); } @JsonNotification(value = LsConstants.SNYK_ADD_TRUSTED_FOLDERS) @@ -342,58 +378,108 @@ public void addTrustedPaths(SnykTrustedFoldersParams param) { var trustedPaths = storedTrustedPaths.split(File.pathSeparator); var pathSet = new HashSet<>(Arrays.asList(trustedPaths)); pathSet.addAll(Arrays.asList(param.getTrustedFolders())); - Preferences.getInstance().store(Preferences.TRUSTED_FOLDERS, pathSet.stream().filter(s -> !s.isBlank()) - .map(s -> s.trim()).distinct().collect(Collectors.joining(File.pathSeparator))); + Preferences.getInstance().store(Preferences.TRUSTED_FOLDERS, + pathSet.stream().filter(s -> !s.isBlank()).map(s -> s.trim()) + .distinct() + .collect(Collectors.joining(File.pathSeparator))); } @JsonNotification(value = LsConstants.SNYK_SCAN) public void snykScan(SnykScanParam param) { - var inProgressKey = new ScanInProgressKey(param.getFolderPath(), param.getProduct()); + var inProgressKey = new ScanInProgressKey(param.getFolderPath(), + param.getProduct()); var scanState = ScanState.getInstance(); SnykIssueCache issueCache = null; if (!param.getFolderPath().isBlank()) { - issueCache = IssueCacheHolder.getInstance().getCacheInstance(param.getFolderPath()); + issueCache = IssueCacheHolder.getInstance() + .getCacheInstance(param.getFolderPath()); } openToolView(); - Set affectedProductTreeNodes = getAffectedProductNodes(param.getProduct(), - param.getFolderPath()); + Set affectedProductTreeNodes = getAffectedProductNodes( + param.getProduct(), param.getFolderPath()); switch (param.getStatus()) { - case SCAN_STATE_IN_PROGRESS: - scanState.setScanInProgress(inProgressKey, true); - break; - case SCAN_STATE_SUCCESS: - scanState.setScanInProgress(inProgressKey, false); - for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { - this.toolView.resetNode(productTreeNode); - addInfoNodes(productTreeNode, param.getFolderPath(), issueCache); - populateFileAndIssueNodes(productTreeNode, issueCache); - } - break; - case SCAN_STATE_ERROR: - scanState.setScanInProgress(inProgressKey, false); - for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { - productTreeNode.setErrorMessage(param.getErrorMessage()); - } - break; - default: - break; + case SCAN_STATE_IN_PROGRESS : + scanState.setScanInProgress(inProgressKey, true); + break; + case SCAN_STATE_SUCCESS : + scanState.setScanInProgress(inProgressKey, false); + for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { + this.toolView.resetNode(productTreeNode); + addInfoNodes(productTreeNode, param.getFolderPath(), + issueCache); + populateFileAndIssueNodes(productTreeNode, issueCache); + } + break; + case SCAN_STATE_ERROR : + scanState.setScanInProgress(inProgressKey, false); + for (ProductTreeNode productTreeNode : affectedProductTreeNodes) { + productTreeNode.setErrorMessage(param.getErrorMessage()); + } + break; + default : + break; } setNodeState(param.getStatus(), affectedProductTreeNodes, issueCache); 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; + } + + SnykShowFixUriDetails uriDetails = SnykShowFixUriDetails.fromURI(uri); + + Issue issue; + if (uriDetails.isValid()) { + issue = getIssueFromCache(uriDetails.filePath(), uriDetails.issueId()); + } else { + SnykLogger.logInfo(String.format( + "Invalid URI: scheme=%s, product=%s, action=%s, issue=%s", + uriDetails.scheme(), uriDetails.product(), uriDetails.action(), uriDetails.issueId())); + return super.showDocument(params); + } + + if (issue == null) { + SnykLogger.logInfo(String.format( + "Issue not found in the issueCache; issueId: %s", uriDetails.issueId())); + return null; + } + + return CompletableFuture.supplyAsync(() -> { + openToolView(); + this.toolView.selectTreeNode(issue, uriDetails.product()); + return new ShowDocumentResult(true); + }); + } + + private Issue getIssueFromCache(String filePath, String issueId) { + SnykIssueCache issueCache = getIssueCache(filePath); + return issueCache.getCodeSecurityIssuesForPath(filePath).stream() + .filter(i -> issueId.equals(i.id())).findFirst().orElse(null); } private void openToolView() { @@ -402,7 +488,8 @@ private void openToolView() { return; } Display.getDefault().syncExec(() -> { - IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + IWorkbenchPage activePage = PlatformUI.getWorkbench() + .getActiveWorkbenchWindow().getActivePage(); try { toolView = (ISnykToolView) activePage.showView(SnykToolView.ID); } catch (PartInitException e) { @@ -412,29 +499,37 @@ private void openToolView() { }); } - private Set getAffectedProductNodes(String snykScanProduct, String folderPath) { + private Set getAffectedProductNodes(String snykScanProduct, + String folderPath) { Preferences pref = Preferences.getInstance(); Set affectedProductTreeNodes = new HashSet<>(); var displayProduct = SCAN_PARAMS_TO_DISPLAYED.get(snykScanProduct); if (displayProduct != null) { - ProductTreeNode productNode = toolView.getProductNode(displayProduct, folderPath); + ProductTreeNode productNode = toolView + .getProductNode(displayProduct, folderPath); if (productNode != null) { affectedProductTreeNodes.add(productNode); } } else { - ProductTreeNode productNode = toolView.getProductNode(DISPLAYED_CODE_SECURITY, folderPath); - if (productNode != null && pref.getBooleanPref(Preferences.ACTIVATE_SNYK_CODE_SECURITY)) { + ProductTreeNode productNode = toolView + .getProductNode(DISPLAYED_CODE_SECURITY, folderPath); + if (productNode != null && pref + .getBooleanPref(Preferences.ACTIVATE_SNYK_CODE_SECURITY)) { affectedProductTreeNodes.add(productNode); } - productNode = toolView.getProductNode(DISPLAYED_CODE_QUALITY, folderPath); - if (productNode != null && pref.getBooleanPref(Preferences.ACTIVATE_SNYK_CODE_QUALITY)) { + productNode = toolView.getProductNode(DISPLAYED_CODE_QUALITY, + folderPath); + if (productNode != null && pref + .getBooleanPref(Preferences.ACTIVATE_SNYK_CODE_QUALITY)) { affectedProductTreeNodes.add(productNode); } } return Collections.unmodifiableSet(affectedProductTreeNodes); } - private void setNodeState(String status, Set affectedProductTreeNodes, SnykIssueCache cache) { + private void setNodeState(String status, + Set affectedProductTreeNodes, + SnykIssueCache cache) { if (affectedProductTreeNodes.isEmpty()) { return; } @@ -456,69 +551,91 @@ private void setNodeState(String status, Set affectedProductTre } } - private void setProductNodeText(Set nodes, String nodeText) { + private void setProductNodeText(Set nodes, + String nodeText) { for (ProductTreeNode productTreeNode : nodes) { toolView.setNodeText(productTreeNode, nodeText); } } - public String getCountsSuffix(ProductTreeNode productTreeNode, SnykIssueCache issueCache) { + public String getCountsSuffix(ProductTreeNode productTreeNode, + SnykIssueCache issueCache) { String product = productTreeNode.getProduct(); - var critical = issueCache.getIssueCountBySeverity(product, SEVERITY_CRITICAL); + var critical = issueCache.getIssueCountBySeverity(product, + SEVERITY_CRITICAL); var high = issueCache.getIssueCountBySeverity(product, SEVERITY_HIGH); - var medium = issueCache.getIssueCountBySeverity(product, SEVERITY_MEDIUM); + var medium = issueCache.getIssueCountBySeverity(product, + SEVERITY_MEDIUM); var low = issueCache.getIssueCountBySeverity(product, SEVERITY_LOW); var total = issueCache.getTotalCount(product); - return String.format("%d unique vulnerabilities: %d critical, %d high, %d medium, %d low", total, critical, - high, medium, low); + return String.format( + "%d unique vulnerabilities: %d critical, %d high, %d medium, %d low", + total, critical, high, medium, low); } private SnykIssueCache getIssueCache(String filePath) { - var issueCache = IssueCacheHolder.getInstance().getCacheInstance(Paths.get(filePath)); + var issueCache = IssueCacheHolder.getInstance() + .getCacheInstance(Paths.get(filePath)); if (issueCache == null) { - throw new IllegalArgumentException("No issue cache for param possible"); + throw new IllegalArgumentException( + "No issue cache for param possible"); } return issueCache; } - private void addInfoNodes(ProductTreeNode productNode, String folderPath, SnykIssueCache issueCache) { + private void addInfoNodes(ProductTreeNode productNode, String folderPath, + SnykIssueCache issueCache) { if (productNode == null) { - SnykLogger.logInfo("given product node is null, not adding info nodes. folderPath: " + folderPath); + SnykLogger.logInfo( + "given product node is null, not adding info nodes. folderPath: " + + folderPath); return; } toolView.removeInfoNodes(productNode); long totalCount = issueCache.getTotalCount(productNode.getProduct()); - long fixableCount = issueCache.getFixableCount(productNode.getProduct()); - long ignoredCount = issueCache.getIgnoredCount(productNode.getProduct()); + long fixableCount = issueCache + .getFixableCount(productNode.getProduct()); + long ignoredCount = issueCache + .getIgnoredCount(productNode.getProduct()); var pref = Preferences.getInstance(); if (totalCount == 0) { - toolView.addInfoNode(productNode, new InfoTreeNode(CONGRATS_NO_ISSUES_FOUND)); + toolView.addInfoNode(productNode, + new InfoTreeNode(CONGRATS_NO_ISSUES_FOUND)); } else { - String text = "✋ " + totalCount + " issue" + getPlural(totalCount) + " found by Snyk"; + String text = "✋ " + totalCount + " issue" + getPlural(totalCount) + + " found by Snyk"; if (ignoredCount > 0) { text += ", " + ignoredCount + " ignored"; } toolView.addInfoNode(productNode, new InfoTreeNode(text)); } if (totalCount > 0 && fixableCount == 0) { - toolView.addInfoNode(productNode, new InfoTreeNode(NO_FIXABLE_ISSUES)); + toolView.addInfoNode(productNode, + new InfoTreeNode(NO_FIXABLE_ISSUES)); } if (totalCount > 0 && fixableCount > 0) { - toolView.addInfoNode(productNode, new InfoTreeNode( - "⚡️ " + fixableCount + " issue" + getPlural(fixableCount) + " can be fixed automatically")); + toolView.addInfoNode(productNode, + new InfoTreeNode("⚡️ " + fixableCount + " issue" + + getPlural(fixableCount) + + " can be fixed automatically")); } if (totalCount > 0 && ignoredCount == totalCount - && pref.getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) + && pref.getBooleanPref( + Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) && pref.getBooleanPref(FILTER_IGNORES_SHOW_OPEN_ISSUES)) { - toolView.addInfoNode(productNode, new InfoTreeNode(ISnykToolView.IGNORED_ISSUES_FILTERED_BUT_AVAILABLE)); + toolView.addInfoNode(productNode, new InfoTreeNode( + ISnykToolView.IGNORED_ISSUES_FILTERED_BUT_AVAILABLE)); } - if (totalCount > 0 && ignoredCount == 0 && pref.getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) + if (totalCount > 0 && ignoredCount == 0 + && pref.getBooleanPref( + Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED) && pref.getBooleanPref(FILTER_IGNORES_SHOW_IGNORED_ISSUES)) { - toolView.addInfoNode(productNode, new InfoTreeNode(ISnykToolView.OPEN_ISSUES_FILTERED_BUT_AVAILABLE)); + toolView.addInfoNode(productNode, new InfoTreeNode( + ISnykToolView.OPEN_ISSUES_FILTERED_BUT_AVAILABLE)); } } @@ -529,17 +646,21 @@ public void publishDiagnostics316(PublishDiagnostics316Param param) { SnykLogger.logInfo("uri for PublishDiagnosticsParams is empty"); return; } - var filePath = LSPEclipseUtils.fromUri(URI.create(uri)).getAbsolutePath(); + var filePath = LSPEclipseUtils.fromUri(URI.create(uri)) + .getAbsolutePath(); if (filePath == null) { - SnykLogger.logError(new InvalidPathException(uri, "couldn't resolve uri " + uri + " to file")); + SnykLogger.logError(new InvalidPathException(uri, + "couldn't resolve uri " + uri + " to file")); return; } populateIssueCache(param, filePath); } - private void populateFileAndIssueNodes(ProductTreeNode productTreeNode, SnykIssueCache issueCache) { - var cacheHashMap = issueCache.getCacheByDisplayProduct(productTreeNode.getProduct()); + private void populateFileAndIssueNodes(ProductTreeNode productTreeNode, + SnykIssueCache issueCache) { + var cacheHashMap = issueCache + .getCacheByDisplayProduct(productTreeNode.getProduct()); List issuesList = new ArrayList<>(); for (var kv : cacheHashMap.entrySet()) { var fileName = kv.getKey(); @@ -548,16 +669,17 @@ 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 } } } - private void populateIssueCache(PublishDiagnostics316Param param, String filePath) { + private void populateIssueCache(PublishDiagnostics316Param param, + String filePath) { var issueCache = getIssueCache(filePath); Diagnostic316[] diagnostics = param.getDiagnostics(); if (diagnostics == null || diagnostics.length == 0) { @@ -570,7 +692,8 @@ private void populateIssueCache(PublishDiagnostics316Param param, String filePat } var snykProduct = LSP_SOURCE_TO_SCAN_PARAMS.get(source); List issueList = new ArrayList<>(); - var isIgnoresEnabled = Preferences.getInstance().getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED); + var isIgnoresEnabled = Preferences.getInstance() + .getBooleanPref(Preferences.IS_GLOBAL_IGNORES_FEATURE_ENABLED); for (var diagnostic : diagnostics) { if (diagnostic.getData() == null) { continue; @@ -583,24 +706,25 @@ private void populateIssueCache(PublishDiagnostics316Param param, String filePat } switch (snykProduct) { - case SCAN_PARAMS_CODE: - issueCache.addCodeIssues(filePath, issueList); - break; - case SCAN_PARAMS_OSS: - issueCache.addOssIssues(filePath, issueList); - break; - case SCAN_PARAMS_IAC: - issueCache.addIacIssues(filePath, issueList); - break; - default: - break; + case SCAN_PARAMS_CODE : + issueCache.addCodeIssues(filePath, issueList); + break; + case SCAN_PARAMS_OSS : + issueCache.addOssIssues(filePath, issueList); + break; + case SCAN_PARAMS_IAC : + issueCache.addIacIssues(filePath, issueList); + break; + default : + break; } } public void reportAnalytics(AbstractTask event) { try { var eventString = om.writeValueAsString(event); - executeCommand(LsConstants.COMMAND_REPORT_ANALYTICS, List.of(eventString)); + executeCommand(LsConstants.COMMAND_REPORT_ANALYTICS, + List.of(eventString)); } catch (Exception e) { SnykLogger.logError(e); } @@ -608,13 +732,27 @@ public void reportAnalytics(AbstractTask event) { } @Override - public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { + public CompletableFuture createProgress( + WorkDoneProgressCreateParams params) { this.progressManager.addProgress(params.getToken().getLeft()); return super.createProgress(params); } + @Override + public void notifyProgress(final ProgressParams params) { + if (params.getValue() == null) { + return; + } + WorkDoneProgressNotification progressNotification = params.getValue().getLeft(); + if (progressNotification != null && progressNotification.getKind() == WorkDoneProgressKind.end) { + this.progressManager.removeProgress(params.getToken().getLeft()); + } + super.notifyProgress(params); + } + /** - * Refresh the token using language server. Waits up to 2s for the token change. + * Refresh the token using language server. Waits up to 2s for the token + * change. * * @return true if token has changed, false if not */ @@ -626,12 +764,15 @@ public boolean refreshOAuthToken() { var token = p.getAuthToken(); final int timeout = 5; CompletableFuture future = CompletableFuture.supplyAsync(() -> { - var result = executeCommand(LsConstants.COMMAND_GET_ACTIVE_USER, new ArrayList<>()); - // we don't wait forever, and if we can't get a user name (refresh token), we're + var result = executeCommand(LsConstants.COMMAND_GET_ACTIVE_USER, + new ArrayList<>()); + // we don't wait forever, and if we can't get a user name (refresh + // token), we're // done. try { result.get(timeout - 1, TimeUnit.SECONDS); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + } catch (InterruptedException | ExecutionException + | TimeoutException e) { Thread.currentThread().interrupt(); return ""; } @@ -646,7 +787,8 @@ public boolean refreshOAuthToken() { }); // wait until token has changed or 2s have passed - var newToken = future.completeOnTimeout(token, 5, TimeUnit.SECONDS).join(); + var newToken = future.completeOnTimeout(token, 5, TimeUnit.SECONDS) + .join(); return !token.equals(newToken); } @@ -679,16 +821,15 @@ public static T convertInstanceOfObject(Object o, Class clazz) { } } - public void setToolWindow(ISnykToolView toolView) { - this.toolView = toolView; - } - public void clearCache() { - List openProjects = ResourceUtils.getAccessibleTopLevelProjects(); + List openProjects = ResourceUtils + .getAccessibleTopLevelProjects(); for (IProject iProject : openProjects) { - IssueCacheHolder.getInstance().getCacheInstance(iProject).clearAll(); + IssueCacheHolder.getInstance().getCacheInstance(iProject) + .clearAll(); try { - iProject.deleteMarkers(MARKER_TYPE, true, IResource.DEPTH_INFINITE); + iProject.deleteMarkers(MARKER_TYPE, true, + IResource.DEPTH_INFINITE); } catch (CoreException e) { SnykLogger.logError(e); } @@ -696,24 +837,9 @@ public void clearCache() { } - public void setProgressMgr(ProgressManager progressMgr) { - this.progressManager = progressMgr; - } - - @Override - public void notifyProgress(final ProgressParams params) { - if (params.getValue() == null) { - return; - } - WorkDoneProgressNotification progressNotification = params.getValue().getLeft(); - if (progressNotification != null && progressNotification.getKind() == WorkDoneProgressKind.end) { - this.progressManager.removeProgress(params.getToken().getLeft()); - } - super.notifyProgress(params); - } - @JsonRequest(value = "workspace/snyk.sdks") - public CompletableFuture> getSdks(WorkspaceFolder workspaceFolder) { + public CompletableFuture> getSdks( + WorkspaceFolder workspaceFolder) { return CompletableFuture.supplyAsync(() -> { List sdks = new SdkHelper().getSdk(workspaceFolder); for (LsSdk lsSdk : sdks) { @@ -723,12 +849,19 @@ public CompletableFuture> getSdks(WorkspaceFolder workspaceFolder) { }); } - public ProgressManager getProgressManager() { - return this.progressManager; + public void setToolWindow(ISnykToolView toolView) { + this.toolView = toolView; } public void setLs(LanguageServer ls) { this.ls = ls; } + public void setProgressMgr(ProgressManager progressMgr) { + this.progressManager = progressMgr; + } + + public ProgressManager getProgressManager() { + return this.progressManager; + } } diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykShowFixUriDetails.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykShowFixUriDetails.java new file mode 100644 index 00000000..ee677c63 --- /dev/null +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/SnykShowFixUriDetails.java @@ -0,0 +1,23 @@ +package io.snyk.languageserver.protocolextension; + +import java.net.URI; + +import io.snyk.eclipse.plugin.utils.UriUtils; + +import static io.snyk.eclipse.plugin.domain.ProductConstants.DIAGNOSTIC_SOURCE_SNYK_CODE; + +public record SnykShowFixUriDetails(String scheme, String filePath, String product, + String action, String issueId) { + public static SnykShowFixUriDetails fromURI(URI uri) { + return new SnykShowFixUriDetails(uri.getScheme(), uri.getPath(), + UriUtils.getDecodedParam(uri, "product"), + UriUtils.getDecodedParam(uri, "action"), + UriUtils.getDecodedParam(uri, "issueId")); + } + + public boolean isValid() { + return ("snyk".equals(this.scheme()) + && DIAGNOSTIC_SOURCE_SNYK_CODE.equals(this.product()) + && "showInDetailPanel".equals(this.action())); + } +} diff --git a/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/Fix.java b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/Fix.java new file mode 100644 index 00000000..38d0ec64 --- /dev/null +++ b/plugin/src/main/java/io/snyk/languageserver/protocolextension/messageObjects/Fix.java @@ -0,0 +1,11 @@ +package io.snyk.languageserver.protocolextension.messageObjects; + +import java.util.Map; + +import com.google.gson.annotations.SerializedName; + +public record Fix( + @SerializedName("fixId") String fixId, + @SerializedName("unifiedDiffsPerFile") Map unifiedDiffsPerFile) { + // no-arg constructor is generated automatically by Java compiler +} \ No newline at end of file diff --git a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java index dd5f16bb..7a4e809e 100644 --- a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java +++ b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykExtendedLanguageClientTest.java @@ -187,12 +187,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)); diff --git a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriDetailsTest.java b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriDetailsTest.java new file mode 100644 index 00000000..87ea2aed --- /dev/null +++ b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriDetailsTest.java @@ -0,0 +1,81 @@ +package io.snyk.languageserver.protocolextension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.jupiter.api.Test; + +import io.snyk.eclipse.plugin.domain.ProductConstants; + +public class SnykUriDetailsTest { + + private static final String BASE_URI_STRING = + "snyk:///test_repos/project-with-vulns/routes/vulnCodeSnippet.ts?product=Snyk+Code&issueId=id&action=showInDetailPanel"; + + private URI createSnykUri() throws URISyntaxException { + return new URI(BASE_URI_STRING); + } + + @Test + void testFromURI() throws URISyntaxException { + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(createSnykUri()); + + assertTrue(details.scheme().equals("snyk")); + assertTrue(details.filePath().equals( + "/test_repos/project-with-vulns/routes/vulnCodeSnippet.ts")); + assertTrue(details.product() + .equals(ProductConstants.DIAGNOSTIC_SOURCE_SNYK_CODE)); + assertTrue(details.action().equals("showInDetailPanel")); + assertTrue(details.issueId().equals("id")); + } + + @Test + void testFromURINullProduct() throws URISyntaxException { + URI uri = new URI("snyk:///test_repos/project-with-vulns/routes/vulnCodeSnippet.ts?issueId=id&action=showInDetailPanel"); + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(uri); + + assertTrue(details.scheme().equals("snyk")); + assertTrue(details.filePath().equals("/test_repos/project-with-vulns/routes/vulnCodeSnippet.ts")); + assertTrue(details.product() == null); // Check if product is null + assertTrue(details.action().equals("showInDetailPanel")); + assertTrue(details.issueId().equals("id")); + } + + @Test + void testFromURIEmptyProduct() throws URISyntaxException { + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(createSnykUri()); + + assertTrue(details.scheme().equals("snyk")); + assertTrue(details.filePath().equals("/test_repos/project-with-vulns/routes/vulnCodeSnippet.ts")); + assertFalse(details.product().isEmpty()); // Check if product is not + // empty + assertTrue(details.action().equals("showInDetailPanel")); + assertTrue(details.issueId().equals("id")); + } + + @Test + void testIsValidTrue() throws URISyntaxException { + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(createSnykUri()); + + assertTrue(details.isValid()); + } + + @Test + void testIsValidFalse() throws URISyntaxException { + URI uri = new URI("http://path/product/showInDetailPanel?issueId=id"); + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(uri); + + assertFalse(details.isValid()); + } + + @Test + void testIsValidProductFalse() throws URISyntaxException { + URI uri = new URI("snyk://path/null/showInDetailPanel?issueId=id"); + SnykShowFixUriDetails details = SnykShowFixUriDetails.fromURI(uri); + + assertFalse(details.isValid()); + } +} diff --git a/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriUtilsTest.java b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriUtilsTest.java new file mode 100644 index 00000000..b8aecb4a --- /dev/null +++ b/tests/src/test/java/io/snyk/languageserver/protocolextension/SnykUriUtilsTest.java @@ -0,0 +1,115 @@ +package io.snyk.languageserver.protocolextension; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import io.snyk.eclipse.plugin.utils.UriUtils; + +class SnykUriUtilsTest { + + @Test + public void getDecodedParam_Returns_Parameter_Value_If_Present_In_Query_String() + throws URISyntaxException { + String query = "product=Snyk+Code&issueId=7642f506c568056a7090d3ceb7b3c2e0&action=showInDetailPanel"; + URI uriWithQuery = new URI("snyk://path/to/resource?" + query); + + var result = UriUtils.getDecodedParam(uriWithQuery, "issueId"); + assertEquals("7642f506c568056a7090d3ceb7b3c2e0", result); + + result = UriUtils.getDecodedParam(uriWithQuery, "action"); + assertEquals("showInDetailPanel", result); + + result = UriUtils.getDecodedParam(uriWithQuery, "product"); + assertEquals("Snyk Code", result); + } + + @Test + void getDecodedParamNullQuery() throws URISyntaxException { + URI uri = new URI( + "snyk:///test_repos/project-with-vulns/routes/vulnCodeSnippet.ts"); + String paramName = "product"; + + assertNull(UriUtils.getDecodedParam(uri, paramName)); + } + + @Test + void getDecodedParamEmptyQuery() throws URISyntaxException { + URI uri = new URI( + "snyk:///test_repos/project-with-vulns/routes/vulnCodeSnippet.ts?product="); + String paramName = "product"; + + assertNull(UriUtils.getDecodedParam(uri, paramName)); + } + + @Test + void getDecodedParamMissingParameter() throws URISyntaxException { + URI uri = new URI( + "snyk:///test_repos/project-with-vulns/routes/vulnCodeSnippet.ts?other=parameter"); + String paramName = "product"; + + assertNull(UriUtils.getDecodedParam(uri, paramName)); + } + + @Test + public void parseQueryString_Returns_Parameters_In_Query_String() + throws Exception { + String query = "product=Snyk+Code&issueId=7642f506c568056a7090d3ceb7b3c2e0&action=showInDetailPanel"; + URI uriWithQuery = new URI("snyk://path/to/resource?" + query); + + var result = UriUtils.getQueryParameters(uriWithQuery.getQuery()); + assertEquals(3, result.size()); + } + + @Test + public void parseQueryString_Returns_Empty_Map_If_Query_String_Is_Empty() + throws URISyntaxException { + var result = UriUtils.getQueryParameters(null); + assertTrue(result.isEmpty()); + } + + @Test + void queryParametersEmptyQueryString() { + Map paramMap = UriUtils.getQueryParameters(""); + + assertTrue(paramMap.isEmpty()); + } + + @Test + void queryParametersNullQueryString() { + Map paramMap = UriUtils.getQueryParameters(null); + + assertTrue(paramMap.isEmpty()); + } + + @Test + void queryParametersSingleParameter() { + String queryString = "product=Snyk+Code"; + Map expectedParamMap = Collections + .singletonMap("product", "Snyk Code"); + + Map paramMap = UriUtils.getQueryParameters(queryString); + + assertEquals(expectedParamMap, paramMap); + } + + @Test + void queryParametersMultipleParameters() { + String queryString = "product=Snyk+Code&other=parameter"; + Map expectedParamMap = new HashMap<>(); + expectedParamMap.put("product", "Snyk Code"); + expectedParamMap.put("other", "parameter"); + + + Map paramMap = UriUtils.getQueryParameters(queryString); + + assertEquals(expectedParamMap, paramMap); + } + +}