diff --git a/CHANGELOG.md b/CHANGELOG.md index 3157b9476..78f524b51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,6 @@ # Snyk Security Changelog - -## [2.9.2] -### Changed -- Generate fix using Snyk DeepCode AI, Apply fix, Retry generating AI fixes - - -## [2.9.1] +## [2.10.0] ### Changed - save git folder config in settings - propagate Jetbrains determined runtime environment to language server @@ -14,12 +8,30 @@ - guard base branch setting against being empty - better error messaging when unexpected loop occurs during initialization - switch downloads to downloads.snyk.io +- added support for net new scans +- allow annotations during IntelliJ indexing +- add gutter icons for Snyk issues +- add option to switch gutter icons on/off +- add color and highlighting setting for Snyk issues +- add dialog to choose reference branch when net new scanning +- always display info nodes +- add option in IntelliJ registry to display tooltips with issue information +- display documentation info when hovering over issue +- Generate fix using Snyk DeepCode AI, Apply fix, Retry generating AI fixes ### Fixes - add name to code vision provider - add flashes for auto-fixable Open Source Issues - show code vision for Open Source also, when Snyk Code is still analysing - clean-up old open source scan functionality +- don't print out exceptions during shutdown of the app/plugin +- if the language server listener is shut down, set initialized to false +- log error stream of language server to idea.log +- show error / warn messages if the project is null (e.g. for offline handling) + +## [2.9.1] +### Fixed +- propagate IntelliJ environment to language server. This should mitigate the issue of not finding package managers during scans. ## [2.9.0] ### Changed diff --git a/README.md b/README.md index b6bfa8ce5..d79c44f21 100644 --- a/README.md +++ b/README.md @@ -2,58 +2,30 @@ description: Use this documentation to get started with the JetBrains plugin. --- -# JetBrains plugins - +# JetBrains plugin -Snyk offers IDE integrations that allow you to use the functionality of Snyk in your Integrated Development Environment. This page describes the Snyk JetBrains plugins. For information about all of the IDE plugins and their use, see [Snyk for IDEs](https://docs.snyk.io/ide-tools) in the docs. +## **Scan early, fix as you develop: elevate your security posture** -Snyk supports JetBrains plugins from version 2020.2 for [IntelliJ IDEA](https://snyk.io/lp/intellij-ide-plugin/) and [WebStorm](https://snyk.io/lp/webstorm-ide-plugin/) as well as Android Studio, AppCode, GoLand, PhpStorm, PyCharm, Rider, and RubyMine. +Integrating security checks early in your development lifecycle helps you pass security reviews seamlessly and avoid expensive fixes down the line. +The Snyk JetBrains plugin allows you to analyze your code, open-source dependencies, Docker images, and Infrastructure as Code (IaC) configurations. With actionable insights directly in your IDE, you can address issues as they arise. -Snyk uses Python in order to scan and find your dependencies. If you are using multiple Python versions, use the -`-command` option to specify the correct Python command for execution. The plugin does not detect the Python version associated with the project. +**Key features:** +* **In-line issue highlighting:** Security issues are flagged directly within your code, categorized by type and severity for quick identification and resolution. +* **Comprehensive scanning:** The extension scans for a wide range of security issues, including: + * [**Open Source Security**](https://snyk.io/product/open-source-security-management/)**:** Detects vulnerabilities and license issues in both direct and transitive open-source dependencies. Automated fix suggestions simplify remediation. Explore more in the [Snyk Open Source documentation](https://docs.snyk.io/scan-using-snyk/snyk-open-source). + * [**Code Security**](https://snyk.io/product/snyk-code/)**:** Identifies security vulnerabilities in your custom code. Explore more in the [Snyk Code documentation](https://docs.snyk.io/scan-using-snyk/snyk-code). + * [**IaC Security**](https://snyk.io/product/infrastructure-as-code-security/)**:** Uncovers configuration issues in your Infrastructure as Code templates (Terraform, Kubernetes, CloudFormation, Azure Resource Manager). Explore more in the [IaC documentation](https://docs.snyk.io/scan-using-snyk/snyk-iac). + * [**Container Security**](https://snyk.io/product/container-vulnerability-management/): Finds security vulnerabilities in your base images; supports all the [operating system distributions supported by Snyk Container](https://docs.snyk.io/scan-using-snyk/snyk-container/how-snyk-container-works/operating-system-distributions-supported-by-snyk-container). See also the [Snyk Container](https://docs.snyk.io/scan-using-snyk/snyk-container) docs. +* **Broad language and framework support:** Snyk Open Source and Snyk Code cover a wide array of package managers, programming languages, and frameworks, with ongoing updates to support the latest technologies. For the most up-to-date information on supported languages, package managers, and frameworks, see the [supported language technologies pages](https://docs.snyk.io/supported-languages-package-managers-and-frameworks). -The Snyk JetBrains plugins provide analysis of your code, containers, and Infrastructure as Code configurations. The plugin is based on the Snyk CLI and also uses Snyk APIs. The plugin supports product features in the CLI for Snyk Open Source and Snyk Container as well as for Snyk Code and Snyk IaC with some limitations. +## How to install and set up the extension -Snyk scans for vulnerabilities and misconfigurations and returns results with security issues categorized by issue type and severity. -For open source, you receive automated algorithm-based fix suggestions for both direct and transitive dependencies. For containers, you can automate upgrades to the most secure base image to quickly resolve numerous vulnerabilities. This single plugin provides a Java vulnerability scanner, a custom code vulnerability scanner, an open-source security scanner, and an application security plugin. +The latest Snyk JetBrains plugin is supported by all JetBrains IDEs 2023.3 or newer. -Snyk scans for the following types of issues: - -[**Open Source Security**](https://snyk.io/product/open-source-security-management/) - security vulnerabilities and license issues in both direct and in-direct (transitive) open-source dependencies pulled into the Snyk Project. See also the [Open Source docs](https://docs.snyk.io/products/snyk-open-source). - -[**Code Security**](https://snyk.io/product/snyk-code/) - security vulnerabilities in your code. See also the [Snyk Code docs](https://docs.snyk.io/products/snyk-code). - -[**Container Security**](https://snyk.io/product/container-vulnerability-management/) - security vulnerabilities in your base images. See also the [Snyk Container docs](https://docs.snyk.io/products/snyk-container). - -[**Infrastructure as Code (IaC) Security**](https://snyk.io/product/infrastructure-as-code-security/) - configuration issues in your IaC templates: Terraform, Kubernetes, CloudFormation, and Azure Resource Manager. See also the [Snyk Infrastructure as Code docs](https://docs.snyk.io/products/snyk-infrastructure-as-code). - -The JetBrains plugins also provide the [**Open Source Advisor**](https://snyk.io/advisor/) to help you find the best package for your next project. Information is provided on the package health of the direct dependencies you are using including popularity, maintenance, risk, and community insights. - -After you complete the installation steps on this page and the [configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) and [authentication](https://docs.snyk.io/ide-tools/jetbrains-plugins/authentication-for-the-jetbrains-plugins) steps on the next two pages, continue by following the instructions in the other JetBrains plugins docs: - -* [Run an analysis with the JetBrains plugins](https://docs.snyk.io/ide-tools/jetbrains-plugins/run-an-analysis-with-the-jetbrains-plugins) -* [JetBrains analysis results: Open Source](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-open-source) -* [JetBrains analysis results: Snyk Code](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-code) -* [JetBrains analysis results: Snyk IaC Configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-iac-configuration) -* [JetBrains analysis results: Snyk Container](https://docs.snyk.io/ide-tools/jetbrains-plugins/jetbrains-analysis-results-snyk-container) -* [How Snyk Container and Kubernetes JetBrains integration works](https://docs.snyk.io/ide-tools/jetbrains-plugins/how-snyk-container-and-kubernetes-jetbrains-integration-works) -* [Filter JetBrains results](https://docs.snyk.io/ide-tools/jetbrains-plugins/filter-jetbrains-results) -* [Troubleshooting for the JetBrains plugin](https://docs.snyk.io/ide-tools/jetbrains-plugins/troubleshooting-for-the-jetbrains-plugin) - - -## Supported languages, package managers, and frameworks - -* For Snyk Open Source, the JetBrains plugin supports the languages and package managers supported by Snyk Open Source and the CLI. For more information, see [Supported languages, frameworks, and feature availability overview, Open Source section](https://docs.snyk.io/scan-applications/supported-languages-and-frameworks/supported-languages-frameworks-and-feature-availability-overview#open-source-and-licensing-snyk-open-source). -* For Snyk Code, the JetBrains plugin supports all the languages and frameworks supported by Snyk Code. For more information, see [Supported languages, frameworks, and feature availability overview, Snyk Code section](https://docs.snyk.io/scan-applications/supported-languages-and-frameworks/supported-languages-frameworks-and-feature-availability-overview#code-analysis-snyk-code). Before scanning your repositories with Snyk Code, ensure you have [enabled Snyk Code](../../../scan-with-snyk/snyk-code/configure-snyk-code.md). -* For Snyk Container: the JetBrains plugin supports all the [operating system distributions supported by Snyk Container](https://docs.snyk.io/products/snyk-container/snyk-container-security-basics/supported-operating-system-distributions). -* For Snyk IaC, the JetBrains plugin supports the following IaC templates: Terraform, Kubernetes, CloudFormation, and Azure Resource Manager. - -## Supported operating systems and architecture - - -Snyk Plugins are not supported on any Operating System that has reached End Of Life (EOL) with the distributor. +An older plugin version is supported by JetBrains IDEs 2020.3 or newer. You can use the Snyk JetBrains plugin in the following environments: @@ -63,23 +35,19 @@ You can use the Snyk JetBrains plugin in the following environments: * Windows: 386, AMD64, and ARM64 * MacOS: AMD64 and ARM64 -## **Install the JetBrains plugin** +Install the plugin at any time free of charge from the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/10972-snyk-vulnerability-scanner) and use it with any Snyk account, including the Free plan. For more information, see the [IDEA plugin installation guide](https://www.jetbrains.com/help/idea/managing-plugins.html). -The Snyk JetBrains plugin is available for installation on the [JetBrains marketplace](https://plugins.jetbrains.com/plugin/10972-snyk-vulnerability-scanner). +When the extension is installed, it automatically downloads the [Snyk CLI,](https://docs.snyk.io/snyk-cli) which includes the [Language Server](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/snyk-language-server). -Install using the IDE plugins library: +Continue by following the instructions in the other JetBrains plugin docs: -1. Open the **Preferences** window in the IDE. -2. Navigate to the **Plugins** tab. -3. In the **Plugins** tab, search for **Snyk**. -4. Select the **Snyk vulnerability scanning** plugin. -5. Click on the **Install** button. -6. When the installation is complete, restart the IDE. - -Select the Snyk vulnerability scanning plugin - -Continue with the steps on the JetBrains [configuration](https://docs.snyk.io/ide-tools/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) page. +* [Configuration, environment variables, and proxy for the JetBrains plugins](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/configuration-environment-variables-and-proxy-for-the-jetbrains-plugins) +* [JetBrains plugin authentication](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/authentication-for-the-jetbrains-plugins) +* [JetBrains plugin folder trust](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/jetbrains-plugin-folder-trust) +* [Run an analysis with the JetBrains plugins](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/run-an-analysis-with-the-jetbrains-plugins) ## Support +For troubleshooting and known issues, see [Troubleshooting for the JetBrains plugin](https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/jetbrains-plugins/troubleshooting-for-the-jetbrains-plugin). + If you need help, submit a [request](https://support.snyk.io/hc/en-us/requests/new) to Snyk Support. diff --git a/build.gradle.kts b/build.gradle.kts index bf62b3dae..37e1c8d12 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -175,7 +175,7 @@ tasks { untilBuild.set(properties("pluginUntilBuild")) val content = File("$projectDir/README.md").readText() - val startIndex = content.indexOf("# JetBrains plugins") + val startIndex = content.indexOf("# JetBrains plugin") val descriptionFromReadme = content.substring(startIndex).lines().joinToString("\n").run { markdownToHTML(this) } pluginDescription.set(descriptionFromReadme) diff --git a/src/main/kotlin/io/snyk/plugin/Severity.kt b/src/main/kotlin/io/snyk/plugin/Severity.kt index 88a4f0e16..cab4acd78 100644 --- a/src/main/kotlin/io/snyk/plugin/Severity.kt +++ b/src/main/kotlin/io/snyk/plugin/Severity.kt @@ -72,15 +72,6 @@ enum class Severity { private const val SEVERITY_MEDIUM = "medium" private const val SEVERITY_LOW = "low" - fun getFromIndex(index: Int): Severity = - when (index) { - 4 -> CRITICAL - 3 -> HIGH - 2 -> MEDIUM - 1 -> LOW - else -> UNKNOWN - } - fun getFromName(name: String): Severity = when (name) { SEVERITY_CRITICAL -> CRITICAL diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt index 07538f7cf..1805c834a 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt @@ -86,7 +86,7 @@ class SnykPostStartupActivity : ProjectActivity { } if (!settings.token.isNullOrBlank() && settings.scanOnSave) { - getSnykTaskQueueService(project)?.scan(true) + getSnykTaskQueueService(project)?.scan() } ExtensionPointsUtil.controllerManager.extensionList.forEach { diff --git a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt index a07a229e6..185bac6fa 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt @@ -28,8 +28,10 @@ class SnykProjectManagerListener : ProjectManagerListener { ls.updateWorkspaceFolders(emptySet(), ls.getWorkspaceFolders(project)) } }.get(TIMEOUT, TimeUnit.SECONDS) - } catch (ignored: RuntimeException) { - logger().info("Project closing clean up took too long", ignored) + } catch (ignored: Exception) { + val logger = logger() + logger.warn("Project closing clean up took longer than $TIMEOUT seconds") + logger.debug(ignored) } } } diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 4cbdc2e06..e6c316a02 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -58,14 +58,9 @@ import java.io.File import java.io.FileNotFoundException import java.net.URI import java.nio.file.Path -import java.security.KeyStore import java.util.Objects.nonNull import java.util.SortedSet import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager import javax.swing.JComponent private val logger = Logger.getInstance("#io.snyk.plugin.UtilsKt") @@ -223,6 +218,8 @@ fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled fun isSnykIaCLSEnabled(): Boolean = false +fun isDocumentationHoverEnabled(): Boolean = Registry.get("snyk.isDocumentationHoverEnabled").asBoolean() + fun getWaitForResultsTimeout(): Long = Registry.intValue( "snyk.timeout.results.waiting", @@ -233,51 +230,41 @@ const val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN = 12L val DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MS = TimeUnit.MILLISECONDS.convert(DEFAULT_TIMEOUT_FOR_SCAN_WAITING_MIN, TimeUnit.MINUTES).toInt() -fun getSSLContext(): SSLContext { - val trustManager = getX509TrustManager() - val sslContext = SSLContext.getInstance("TLSv1.2") - sslContext.init(null, arrayOf(trustManager), null) - return sslContext -} - -fun getX509TrustManager(): X509TrustManager { - val trustManagerFactory: TrustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm() - ) - trustManagerFactory.init(null as KeyStore?) - val trustManagers: Array = trustManagerFactory.trustManagers - check(!(trustManagers.size != 1 || trustManagers[0] !is X509TrustManager)) { - ("Unexpected default trust managers:${trustManagers.contentToString()}") - } - return trustManagers[0] as X509TrustManager -} - -fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? = - if (!virtualFile.isValid || project.isDisposed) { +fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): PsiFile? { + return if (!virtualFile.isValid || project.isDisposed) { null } else { try { - PsiManager.getInstance(project).findFile(virtualFile) + var psiFile : PsiFile? = null + ReadAction.run { + psiFile = PsiManager.getInstance(project).findFile(virtualFile) + } + return psiFile } catch (ignored: Throwable) { null } } +} fun refreshAnnotationsForOpenFiles(project: Project) { if (project.isDisposed || ApplicationManager.getApplication().isDisposed) return - VirtualFileManager.getInstance().asyncRefresh() + runAsync { + VirtualFileManager.getInstance().asyncRefresh() - val openFiles = FileEditorManager.getInstance(project).openFiles + val openFiles = FileEditorManager.getInstance(project).openFiles - ApplicationManager.getApplication().invokeLater { - if (!project.isDisposed) { - project.service().invalidateProvider(CodeVisionHost.LensInvalidateSignal(null)) + ApplicationManager.getApplication().invokeLater { + if (!project.isDisposed) { + project.service().invalidateProvider(CodeVisionHost.LensInvalidateSignal(null)) + } } - } - openFiles.forEach { - val psiFile = findPsiFileIgnoringExceptions(it, project) - if (psiFile != null) { - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + openFiles.forEach { + val psiFile = findPsiFileIgnoringExceptions(it, project) + if (psiFile != null) { + invokeLater { + DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + } + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt index e557d560e..402ae64b9 100644 --- a/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt +++ b/src/main/kotlin/io/snyk/plugin/extensions/SnykControllerImpl.kt @@ -13,7 +13,7 @@ class SnykControllerImpl(val project: Project) : SnykController { * scan enqueues a scan of the project for vulnerabilities. */ override fun scan() { - getSnykTaskQueueService(project)?.scan(false) + getSnykTaskQueueService(project)?.scan() } /** diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index fa3a2ba69..b2db93677 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -35,8 +35,10 @@ class SnykApplicationSettingsStateService : PersistentStateComponent() .warn("Language server shutdown for download took too long, couldn't shutdown", e) diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index c9e9b09f7..165b15f0b 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -3,12 +3,14 @@ package io.snyk.plugin.settings import com.intellij.notification.Notification import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.progress.runBackgroundableTask import com.intellij.openapi.project.Project import io.snyk.plugin.events.SnykProductsOrSeverityListener import io.snyk.plugin.events.SnykResultsFilteringListener import io.snyk.plugin.events.SnykSettingsListener +import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykProjectSettingsService import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getSnykToolWindowPanel @@ -18,6 +20,7 @@ import io.snyk.plugin.isUrlValid import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.SnykSettingsDialog +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import javax.swing.JComponent @@ -27,8 +30,7 @@ class SnykProjectSettingsConfigurable( private val settingsStateService get() = pluginSettings() - var snykSettingsDialog: SnykSettingsDialog = - SnykSettingsDialog(project, settingsStateService, this) + var snykSettingsDialog: SnykSettingsDialog = SnykSettingsDialog(project, settingsStateService, this) override fun getId(): String = "io.snyk.plugin.settings.SnykProjectSettingsConfigurable" @@ -39,8 +41,6 @@ class SnykProjectSettingsConfigurable( override fun isModified(): Boolean = isCoreParamsModified() || isIgnoreUnknownCAModified() || - isSendUsageAnalyticsModified() || - isCrashReportingModified() || snykSettingsDialog.isScanTypeChanged() || snykSettingsDialog.isSeverityEnablementChanged() || snykSettingsDialog.isIssueOptionChanged() || @@ -49,6 +49,8 @@ class SnykProjectSettingsConfigurable( snykSettingsDialog.getCliBaseDownloadURL() != settingsStateService.cliBaseDownloadURL || snykSettingsDialog.isScanOnSaveEnabled() != settingsStateService.scanOnSave || snykSettingsDialog.getCliReleaseChannel() != settingsStateService.cliReleaseChannel || + snykSettingsDialog.getNetNewIssuesSelected() != settingsStateService.netNewIssues || + isAuthenticationMethodModified() private fun isAuthenticationMethodModified() = @@ -89,9 +91,6 @@ class SnykProjectSettingsConfigurable( settingsStateService.organization = snykSettingsDialog.getOrganization() settingsStateService.ignoreUnknownCA = snykSettingsDialog.isIgnoreUnknownCA() - settingsStateService.usageAnalyticsEnabled = snykSettingsDialog.isUsageAnalyticsEnabled() - settingsStateService.crashReportingEnabled = snykSettingsDialog.isCrashReportingEnabled() - settingsStateService.manageBinariesAutomatically = snykSettingsDialog.manageBinariesAutomatically() settingsStateService.cliPath = snykSettingsDialog.getCliPath().trim() settingsStateService.cliBaseDownloadURL = snykSettingsDialog.getCliBaseDownloadURL().trim() @@ -105,6 +104,10 @@ class SnykProjectSettingsConfigurable( if (isProjectSettingsAvailable(project)) { val snykProjectSettingsService = getSnykProjectSettingsService(project) snykProjectSettingsService?.additionalParameters = snykSettingsDialog.getAdditionalParameters() + val fcs = service() + fcs.getAllForProject(project) + .map { it.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) } + .forEach { fcs.addFolderConfig(it) } } runBackgroundableTask("processing config changes", project, true) { @@ -116,6 +119,14 @@ class SnykProjectSettingsConfigurable( handleReleaseChannelChanged() } + if (snykSettingsDialog.getNetNewIssuesSelected() != pluginSettings().netNewIssues) { + settingsStateService.netNewIssues = snykSettingsDialog.getNetNewIssuesSelected() + val cache = getSnykCachedResults(project) + cache?.currentOSSResultsLS?.clear() + cache?.currentSnykCodeResultsLS?.clear() + // TODO when we enable iac add cache cleaning here, when we have container, we can use cleanCaches() + } + LanguageServerWrapper.getInstance().updateConfiguration() } @@ -135,27 +146,26 @@ class SnykProjectSettingsConfigurable( private fun handleReleaseChannelChanged() { settingsStateService.cliReleaseChannel = snykSettingsDialog.getCliReleaseChannel().trim() var notification: Notification? = null - val downloadAction = - object : AnAction("Download") { - override fun actionPerformed(e: AnActionEvent) { - getSnykTaskQueueService(project)?.downloadLatestRelease(true) - ?: SnykBalloonNotificationHelper.showWarn("Could not download Snyk CLI", project) - notification?.expire() - } + val downloadAction = object : AnAction("Download") { + override fun actionPerformed(e: AnActionEvent) { + getSnykTaskQueueService(project)?.downloadLatestRelease(true) ?: SnykBalloonNotificationHelper.showWarn( + "Could not download Snyk CLI", + project + ) + notification?.expire() } - val noAction = - object : AnAction("Cancel") { - override fun actionPerformed(e: AnActionEvent) { - notification?.expire() - } + } + val noAction = object : AnAction("Cancel") { + override fun actionPerformed(e: AnActionEvent) { + notification?.expire() } - notification = - SnykBalloonNotificationHelper.showInfo( - "You changed the release channel. Would you like to download a new Snyk CLI now?", - project, - downloadAction, - noAction, - ) + } + notification = SnykBalloonNotificationHelper.showInfo( + "You changed the release channel. Would you like to download a new Snyk CLI now?", + project, + downloadAction, + noAction, + ) } private fun isTokenModified(): Boolean = snykSettingsDialog.getToken() != settingsStateService.token @@ -169,13 +179,8 @@ class SnykProjectSettingsConfigurable( private fun isIgnoreUnknownCAModified(): Boolean = snykSettingsDialog.isIgnoreUnknownCA() != settingsStateService.ignoreUnknownCA - private fun isSendUsageAnalyticsModified(): Boolean = - snykSettingsDialog.isUsageAnalyticsEnabled() != settingsStateService.usageAnalyticsEnabled - - private fun isCrashReportingModified(): Boolean = - snykSettingsDialog.isCrashReportingEnabled() != settingsStateService.crashReportingEnabled - private fun isAdditionalParametersModified(): Boolean = - isProjectSettingsAvailable(project) && - snykSettingsDialog.getAdditionalParameters() != getSnykProjectSettingsService(project)?.additionalParameters + isProjectSettingsAvailable(project) && snykSettingsDialog.getAdditionalParameters() != getSnykProjectSettingsService( + project + )?.additionalParameters } diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt new file mode 100644 index 000000000..547bc2401 --- /dev/null +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -0,0 +1,83 @@ +package io.snyk.plugin.ui + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.util.ui.GridBag +import com.intellij.util.ui.JBUI +import org.jetbrains.concurrency.runAsync +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.LanguageServerWrapper +import java.awt.GridBagConstraints +import java.awt.GridBagLayout +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JPanel + + +class BranchChooserComboBoxDialog(val project: Project) : DialogWrapper(true) { + var comboBoxes: MutableList> = mutableListOf() + + init { + init() + title = "Choose base branch for net-new issue scanning" + } + + override fun createCenterPanel(): JComponent { + val folderConfigs = service().getAllForProject(project) + folderConfigs.forEach { + val comboBox = ComboBox(it.localBranches.sorted().toTypedArray()) + comboBox.selectedItem = it.baseBranch + comboBox.name = it.folderPath + comboBoxes.add(comboBox) + } + val gridBagLayout = GridBagLayout() + val dialogPanel = JPanel(gridBagLayout) + val gridBag = GridBag() + gridBag.defaultFill = GridBagConstraints.HORIZONTAL + gridBag.insets = JBUI.insets(20) + comboBoxes.forEach { + dialogPanel.add(JLabel("Base Branch for ${it.name}: ")) + dialogPanel.add(it, gridBag.nextLine()) + } + return dialogPanel + } + + override fun doOKAction() { + execute() + super.doOKAction() + } + + fun execute() { + val folderConfigSettings = service() + comboBoxes.forEach { + val folderConfig: FolderConfig? = folderConfigSettings.getFolderConfig(it.name) + if (folderConfig == null) { + SnykBalloonNotificationHelper.showError( + "Unexpectedly cannot retrieve folder config for ${it.name} for base branch updating.", + project + ) + return@forEach + } + + val baseBranch = it.selectedItem!!.toString() // validation makes sure it is not null and not empty + folderConfigSettings.addFolderConfig(folderConfig.copy(baseBranch = baseBranch)) + } + runAsync { + LanguageServerWrapper.getInstance().updateConfiguration() + LanguageServerWrapper.getInstance().sendScanCommand(project) + } + } + + override fun doValidate(): ValidationInfo? { + comboBoxes.forEach { + if (it.selectedItem == null || it.selectedItem?.toString()?.isEmpty() == true) { + return ValidationInfo("Please select a base branch for ${it.name}", it) + } + } + return null + } +} diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt index 108de1870..d5781f88e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotificationHelper.kt @@ -30,10 +30,10 @@ object SnykBalloonNotificationHelper { showNotification(message, project, NotificationType.ERROR, *actions) } - fun showInfo(message: String, project: Project, vararg actions: AnAction) = + fun showInfo(message: String, project: Project?, vararg actions: AnAction) = showNotification(message, project, NotificationType.INFORMATION, *actions) - fun showWarn(message: String, project: Project, vararg actions: AnAction) = + fun showWarn(message: String, project: Project?, vararg actions: AnAction) = showNotification(message, project, NotificationType.WARNING, *actions) private fun showNotification( diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index b63979de8..8c90a1740 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -24,7 +24,6 @@ import com.intellij.ui.components.JBTextField import com.intellij.ui.components.fields.ExpandableTextField import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.util.minimumWidth import com.intellij.ui.util.preferredWidth import com.intellij.uiDesigner.core.Spacer import com.intellij.util.Alarm @@ -52,6 +51,7 @@ import snyk.SnykBundle import snyk.common.lsp.FolderConfigSettings import java.awt.GridBagConstraints import java.awt.GridBagLayout +import java.awt.Insets import java.io.File.separator import java.util.Objects.nonNull import java.util.function.Supplier @@ -87,8 +87,6 @@ class SnykSettingsDialog( private val useTokenAuthentication = ComboBox(arrayOf("OAuth2 authentication", "Token authentication")).apply { this.isEditable = false - this.preferredWidth = 200 - this.minimumWidth = 200 } private val customEndpointTextField = JTextField().apply { preferredWidth = tokenTextField.preferredWidth } @@ -99,9 +97,7 @@ class SnykSettingsDialog( } private val ignoreUnknownCACheckBox: JCheckBox = JCheckBox().apply { toolTipText = "Enabling this causes SSL certificate validation to be disabled" } - private val usageAnalyticsCheckBox: JCheckBox = - JCheckBox().apply { toolTipText = "If enabled, send analytics to Amplitude" } - private val crashReportingCheckBox = JCheckBox().apply { toolTipText = "If enabled, send error reports to Sentry" } + private val scanOnSaveCheckbox = JCheckBox().apply { toolTipText = "If enabled, automatically scan on save, start-up and configuration change" } private val additionalParametersTextField: JTextField = @@ -118,6 +114,8 @@ class SnykSettingsDialog( private val cliPathTextBoxWithFileBrowser = TextFieldWithBrowseButton() private val channels = listOf("stable", "rc", "preview").toArray(emptyArray()) private val cliReleaseChannelDropDown = ComboBox(channels).apply { this.isEditable = true } + private val newIssues = listOf("All issues", "Net new issues").toArray(emptyArray()) + private val netNewIssuesDropDown = ComboBox(newIssues).apply { this.isEditable = false } private val cliBaseDownloadUrlTextField = JBTextField() private val baseBranchInfoLabel = JBLabel("Base branch: ") @@ -172,8 +170,6 @@ class SnykSettingsDialog( customEndpointTextField.text = applicationSettings.customEndpointUrl organizationTextField.text = applicationSettings.organization ignoreUnknownCACheckBox.isSelected = applicationSettings.ignoreUnknownCA - usageAnalyticsCheckBox.isSelected = applicationSettings.usageAnalyticsEnabled - crashReportingCheckBox.isSelected = applicationSettings.crashReportingEnabled manageBinariesAutomatically.isSelected = applicationSettings.manageBinariesAutomatically cliPathTextBoxWithFileBrowser.text = applicationSettings.cliPath @@ -182,8 +178,9 @@ class SnykSettingsDialog( scanOnSaveCheckbox.isSelected = applicationSettings.scanOnSave cliReleaseChannelDropDown.selectedItem = applicationSettings.cliReleaseChannel - baseBranchInfoLabel.text = ""+service().getAll() - .values.joinToString("") { "Base branch for ${it.folderPath}: ${it.baseBranch}" }+"" + baseBranchInfoLabel.text = service().getAll() + .values.joinToString("\n") { "Base branch for ${it.folderPath}: ${it.baseBranch}" } + netNewIssuesDropDown.selectedItem = applicationSettings.netNewIssues } } @@ -206,7 +203,7 @@ class SnykSettingsDialog( /** General settings ------------------ */ - val generalSettingsPanel = JPanel(UIGridLayoutManager(7, 4, JBUI.emptyInsets(), -1, -1)) + val generalSettingsPanel = JPanel(UIGridLayoutManager(7, 3, JBUI.emptyInsets(), -1, -1)) generalSettingsPanel.border = IdeBorderFactory.createTitledBorder("General settings") rootPanel.add( @@ -225,6 +222,7 @@ class SnykSettingsDialog( authenticationMethodLabel, baseGridConstraints( row = 0, + column = 0, indent = 0, anchor = UIGridConstraints.ANCHOR_WEST ), @@ -235,10 +233,10 @@ class SnykSettingsDialog( baseGridConstraints( row = 0, column = 1, - colSpan = 3, + colSpan = 1, indent = 0, anchor = UIGridConstraints.ANCHOR_WEST, - fill = UIGridConstraints.FILL_HORIZONTAL, + fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, ), ) @@ -254,10 +252,10 @@ class SnykSettingsDialog( baseGridConstraints( row = 1, column = 1, - colSpan = 3, + colSpan = 2, indent = 0, anchor = UIGridConstraints.ANCHOR_WEST, - fill = UIGridConstraints.FILL_HORIZONTAL, + fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, ), ) @@ -285,10 +283,10 @@ class SnykSettingsDialog( baseGridConstraints( row = 3, column = 1, - colSpan = 3, + colSpan = 1, anchor = UIGridConstraints.ANCHOR_WEST, fill = UIGridConstraints.FILL_NONE, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, indent = 0, ), ) @@ -312,10 +310,10 @@ class SnykSettingsDialog( baseGridConstraints( row = 4, column = 1, - colSpan = 3, + colSpan = 1, anchor = UIGridConstraints.ANCHOR_WEST, fill = UIGridConstraints.FILL_NONE, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, indent = 0, ), ) @@ -326,7 +324,7 @@ class SnykSettingsDialog( baseGridConstraints( row = 5, column = 1, - colSpan = 3, + colSpan = 1, anchor = UIGridConstraints.ANCHOR_WEST, fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, @@ -349,10 +347,10 @@ class SnykSettingsDialog( baseGridConstraints( row = 6, column = 1, - colSpan = 2, + colSpan = 1, anchor = UIGridConstraints.ANCHOR_WEST, - fill = UIGridConstraints.FILL_HORIZONTAL, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, indent = 0, ), ) @@ -369,8 +367,10 @@ class SnykSettingsDialog( organizationContextHelpLabel, baseGridConstraintsAnchorWest( row = 6, - column = 3, + column = 2, indent = 0, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, ), ) @@ -412,15 +412,16 @@ class SnykSettingsDialog( ), ) } - val productAndSeveritiesPanel = JPanel(UIGridLayoutManager(1, 2, JBUI.emptyInsets(), 30, -1)) + + val productAndSeveritiesPanel = JPanel(UIGridLayoutManager(2, 2, JBUI.emptyInsets(), 30, -1)) rootPanel.add( productAndSeveritiesPanel, baseGridConstraints( row = 2, anchor = UIGridConstraints.ANCHOR_NORTHWEST, - fill = UIGridConstraints.FILL_HORIZONTAL, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) @@ -429,14 +430,15 @@ class SnykSettingsDialog( scanTypesPanel, baseGridConstraints( row = 0, + column = 0, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_NONE, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) - val severitiesPanel = JPanel(UIGridLayoutManager(5, 4, JBUI.emptyInsets(), -1, -1)) + val severitiesPanel = JPanel(UIGridLayoutManager(1, 1, JBUI.emptyInsets(), -1, -1)) severitiesPanel.border = IdeBorderFactory.createTitledBorder("Severity selection") productAndSeveritiesPanel.add( @@ -446,7 +448,7 @@ class SnykSettingsDialog( column = 1, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_NONE, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) @@ -457,12 +459,14 @@ class SnykSettingsDialog( row = 0, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_NONE, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, - vSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + vSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) + createNetNewPanel(productAndSeveritiesPanel) + /** Project settings ------------------ */ if (isProjectSettingsAvailable(project)) { @@ -474,7 +478,7 @@ class SnykSettingsDialog( baseGridConstraints( row = 3, fill = UIGridConstraints.FILL_BOTH, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) @@ -493,8 +497,9 @@ class SnykSettingsDialog( baseGridConstraints( row = 0, column = 1, + colSpan = 2, anchor = UIGridConstraints.ANCHOR_WEST, - fill = UIGridConstraints.FILL_HORIZONTAL, + fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, indent = 0, ), @@ -502,19 +507,6 @@ class SnykSettingsDialog( additionalParametersLabel.labelFor = additionalParametersTextField - projectSettingsPanel.add( - baseBranchInfoLabel, - baseGridConstraints( - 1, - 0, - anchor = UIGridConstraints.ANCHOR_WEST, - fill = UIGridConstraints.FILL_HORIZONTAL, - hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_GROW, - colSpan = 2, - indent = 0 - ) - ) - val projectSettingsSpacer = Spacer() projectSettingsPanel.add( projectSettingsSpacer, @@ -523,6 +515,7 @@ class SnykSettingsDialog( fill = UIGridConstraints.FILL_VERTICAL, hSizePolicy = 1, vSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + colSpan = 2, indent = 0, ), ) @@ -538,9 +531,9 @@ class SnykSettingsDialog( rootPanel.add( userExperiencePanel, baseGridConstraints( - row = 5, + row = 6, anchor = UIGridConstraints.ANCHOR_NORTHWEST, - fill = UIGridConstraints.FILL_HORIZONTAL, + fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, indent = 0, ), @@ -556,33 +549,75 @@ class SnykSettingsDialog( ), ) - usageAnalyticsCheckBox.text = "Send usage statistics to Snyk" - userExperiencePanel.add( - usageAnalyticsCheckBox, + /** Spacer ------------------ */ + + val generalSettingsSpacer = Spacer() + rootPanel.add( + generalSettingsSpacer, + panelGridConstraints( + row = 5, + ), + ) + } + + private fun createNetNewPanel(productAndSeveritiesPanel: JPanel) { + val netNewIssuesPanel = JPanel(UIGridLayoutManager(2, 2, JBUI.insets(Insets(5, 0, 20, 0)), -1, -1)) + + productAndSeveritiesPanel.add( + netNewIssuesPanel, baseGridConstraints( row = 1, + column = 0, + colSpan = 2, anchor = UIGridConstraints.ANCHOR_NORTHWEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) - crashReportingCheckBox.text = "Send error reports to Snyk" - userExperiencePanel.add( - crashReportingCheckBox, + val newNewIssuesLabel = JLabel("All Issues Vs Net New Issues:") + newNewIssuesLabel.labelFor = netNewIssuesDropDown + netNewIssuesPanel.add( + newNewIssuesLabel, baseGridConstraints( - row = 2, - anchor = UIGridConstraints.ANCHOR_NORTHWEST, + row = 0, + column = 0, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, indent = 0, ), ) - /** Spacer ------------------ */ + netNewIssuesPanel.add( + netNewIssuesDropDown, + baseGridConstraints( + row = 0, + column = 1, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + indent = 0, + ), + ) - val generalSettingsSpacer = Spacer() - rootPanel.add( - generalSettingsSpacer, - panelGridConstraints( - row = 5, + val netNewIssuesText = + JLabel( + "Specifies whether to see only net new issues or all issues. " + + "Only applies to Code Security and Code Quality." + ).apply { font = FontUtil.minusOne(this.font) } + + netNewIssuesPanel.add( + netNewIssuesText, + baseGridConstraints( + row = 1, + column = 0, + colSpan = 2, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + indent = 0, ), ) } @@ -599,9 +634,9 @@ class SnykSettingsDialog( rootPanel.add( executableSettingsPanel, baseGridConstraints( - row = 4, + row = 5, anchor = UIGridConstraints.ANCHOR_NORTHWEST, - fill = UIGridConstraints.FILL_HORIZONTAL, + fill = UIGridConstraints.FILL_NONE, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, indent = 0, ), @@ -615,7 +650,7 @@ class SnykSettingsDialog( introLabel.font = FontUtil.minusOne(introLabel.font) executableSettingsPanel.add( introLabel, - gb.nextLine(), + gb.nextLine() ) cliBaseDownloadUrlTextField.toolTipText = "The default URL is https://downloads.snyk.io. " + @@ -700,12 +735,8 @@ class SnykSettingsDialog( fun isIgnoreUnknownCA(): Boolean = ignoreUnknownCACheckBox.isSelected - fun isUsageAnalyticsEnabled(): Boolean = usageAnalyticsCheckBox.isSelected - fun isScanOnSaveEnabled(): Boolean = scanOnSaveCheckbox.isSelected - fun isCrashReportingEnabled(): Boolean = crashReportingCheckBox.isSelected - fun isScanTypeChanged(): Boolean = scanTypesPanel.isModified() fun saveScanTypeChanges() = scanTypesPanel.apply() @@ -784,5 +815,7 @@ class SnykSettingsDialog( fun getCliReleaseChannel(): String = cliReleaseChannelDropDown.selectedItem as String + fun getNetNewIssuesSelected(): String = netNewIssuesDropDown.selectedItem as String + fun getUseTokenAuthentication(): Boolean = useTokenAuthentication.selectedIndex == 1 } diff --git a/src/main/kotlin/io/snyk/plugin/ui/actions/SnykRunScanAction.kt b/src/main/kotlin/io/snyk/plugin/ui/actions/SnykRunScanAction.kt index 673e4ff6b..bb3adffb0 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/actions/SnykRunScanAction.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/actions/SnykRunScanAction.kt @@ -16,7 +16,7 @@ import io.snyk.plugin.pluginSettings class SnykRunScanAction : AnAction(AllIcons.Actions.Execute), DumbAware { override fun actionPerformed(actionEvent: AnActionEvent) { - getSnykTaskQueueService(actionEvent.project!!)?.scan(false) + getSnykTaskQueueService(actionEvent.project!!)?.scan() } override fun update(actionEvent: AnActionEvent) { diff --git a/src/main/kotlin/io/snyk/plugin/ui/settings/IssueViewOptionsPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/settings/IssueViewOptionsPanel.kt index b40abd61b..94d464745 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/settings/IssueViewOptionsPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/settings/IssueViewOptionsPanel.kt @@ -28,7 +28,7 @@ class IssueViewOptionsPanel( .actionListener{ _, it -> if (canBeChanged(it, it.isSelected)) { currentOpenIssuesEnabled = it.isSelected - getSnykTaskQueueService(project)?.scan(false) + getSnykTaskQueueService(project)?.scan() } } // bindSelected is needed to trigger apply() on the settings dialog that this panel is rendered in @@ -44,7 +44,7 @@ class IssueViewOptionsPanel( .actionListener{ _, it -> if (canBeChanged(it, it.isSelected)) { currentIgnoredIssuesEnabled = it.isSelected - getSnykTaskQueueService(project)?.scan(false) + getSnykTaskQueueService(project)?.scan() } } .bindSelected(settings::ignoredIssuesEnabled) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt index 25389d06c..eba4422fc 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt @@ -15,14 +15,25 @@ import java.util.concurrent.TimeUnit */ @Service(Service.Level.APP, Service.Level.PROJECT) class SnykPluginDisposable : Disposable, AppLifecycleListener { + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + + override fun dispose() { + disposed = true + } + companion object { @NotNull - fun getInstance(): Disposable { + fun getInstance(): SnykPluginDisposable { return ApplicationManager.getApplication().getService(SnykPluginDisposable::class.java) } @NotNull - fun getInstance(@NotNull project: Project): Disposable { + fun getInstance(@NotNull project: Project): SnykPluginDisposable { return project.getService(SnykPluginDisposable::class.java) } } @@ -33,7 +44,7 @@ class SnykPluginDisposable : Disposable, AppLifecycleListener { override fun appClosing() { try { - LanguageServerWrapper.getInstance().shutdown().get(2, TimeUnit.SECONDS) + LanguageServerWrapper.getInstance().shutdown() } catch (ignored: Exception) { // do nothing } @@ -41,12 +52,9 @@ class SnykPluginDisposable : Disposable, AppLifecycleListener { override fun appWillBeClosed(isRestart: Boolean) { try { - LanguageServerWrapper.getInstance().shutdown().get(2, TimeUnit.SECONDS) + LanguageServerWrapper.getInstance().shutdown() } catch (ignored: Exception) { // do nothing } } - - override fun dispose() = Unit - } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index c82ac0c9e..0c7bd392d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -3,6 +3,7 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project @@ -40,6 +41,7 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.snykToolWindow +import io.snyk.plugin.ui.BranchChooserComboBoxDialog import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.expandTreeNodeRecursively import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode @@ -52,7 +54,9 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootTreeNodeBase +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanel import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel @@ -116,7 +120,7 @@ class SnykToolWindowPanel( * */ private var smartReloadMode = false - var navigateToSourceEnabled = true + var triggerSelectionListeners = true private val treeNodeStub = object : RootTreeNodeBase("", project) { @@ -175,19 +179,19 @@ class SnykToolWindowPanel( override fun scanningIacFinished(iacResult: IacResult) { ApplicationManager.getApplication().invokeLater { displayIacResults(iacResult) - if (iacResult.getVisibleErrors().isNotEmpty()) { - notifyAboutErrorsIfNeeded(ProductType.IAC, iacResult) - } - refreshAnnotationsForOpenFiles(project) } + if (iacResult.getVisibleErrors().isNotEmpty()) { + notifyAboutErrorsIfNeeded(ProductType.IAC, iacResult) + } + refreshAnnotationsForOpenFiles(project) } override fun scanningContainerFinished(containerResult: ContainerResult) { ApplicationManager.getApplication().invokeLater { displayContainerResults(containerResult) - notifyAboutErrorsIfNeeded(ProductType.CONTAINER, containerResult) - refreshAnnotationsForOpenFiles(project) } + notifyAboutErrorsIfNeeded(ProductType.CONTAINER, containerResult) + refreshAnnotationsForOpenFiles(project) } private fun notifyAboutErrorsIfNeeded( @@ -210,38 +214,38 @@ class SnykToolWindowPanel( override fun scanningIacError(snykError: SnykError) { var iacResultsCount: Int? = null - ApplicationManager.getApplication().invokeLater { - if (snykError.code != null && ignorableErrorCodes.contains(snykError.code)) { - iacResultsCount = NODE_NOT_SUPPORTED_STATE - } else { - SnykBalloonNotificationHelper.showError(snykError.message, project) - if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { - pluginSettings().token = null - } + if (snykError.code != null && ignorableErrorCodes.contains(snykError.code)) { + iacResultsCount = NODE_NOT_SUPPORTED_STATE + } else { + SnykBalloonNotificationHelper.showError(snykError.message, project) + if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { + pluginSettings().token = null } + } + ApplicationManager.getApplication().invokeLater { removeAllChildren(listOf(rootIacIssuesTreeNode)) updateTreeRootNodesPresentation(iacResultsCount = iacResultsCount) chooseMainPanelToDisplay() - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } override fun scanningContainerError(snykError: SnykError) { var containerResultsCount: Int? = null - ApplicationManager.getApplication().invokeLater { - if (snykError == ContainerService.NO_IMAGES_TO_SCAN_ERROR) { - containerResultsCount = NODE_NOT_SUPPORTED_STATE - } else { - SnykBalloonNotificationHelper.showError(snykError.message, project) - if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { - pluginSettings().token = null - } + if (snykError == ContainerService.NO_IMAGES_TO_SCAN_ERROR) { + containerResultsCount = NODE_NOT_SUPPORTED_STATE + } else { + SnykBalloonNotificationHelper.showError(snykError.message, project) + if (snykError.message.startsWith(AUTH_FAILED_TEXT)) { + pluginSettings().token = null } + } + ApplicationManager.getApplication().invokeLater { removeAllChildren(listOf(rootContainerIssuesTreeNode)) updateTreeRootNodesPresentation(containerResultsCount = containerResultsCount) chooseMainPanelToDisplay() - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }, ) @@ -252,20 +256,20 @@ class SnykToolWindowPanel( SnykResultsFilteringListener.SNYK_FILTERING_TOPIC, object : SnykResultsFilteringListener { override fun filtersChanged() { + val codeResultsLS = + getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return ApplicationManager.getApplication().invokeLater { - val codeResultsLS = - getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return@invokeLater scanListenerLS.displaySnykCodeResults(codeResultsLS) } + val ossResultsLS = + getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return ApplicationManager.getApplication().invokeLater { - val ossResultsLS = - getSnykCachedResultsForProduct(project, ProductType.OSS) ?: return@invokeLater scanListenerLS.displayOssResults(ossResultsLS) } + val snykCachedResults = getSnykCachedResults(project) ?: return ApplicationManager.getApplication().invokeLater { - val snykCachedResults = getSnykCachedResults(project) ?: return@invokeLater snykCachedResults.currentIacResult?.let { displayIacResults(it) } snykCachedResults.currentContainerResult?.let { displayContainerResults(it) } } @@ -328,13 +332,18 @@ class SnykToolWindowPanel( private fun updateDescriptionPanelBySelectedTreeNode() { val capturedSmartReloadMode = smartReloadMode - val capturedNavigateToSourceEnabled = navigateToSourceEnabled + val capturedNavigateToSourceEnabled = triggerSelectionListeners ApplicationManager.getApplication().invokeLater { descriptionPanel.removeAll() val selectionPath = vulnerabilitiesTree.selectionPath if (nonNull(selectionPath)) { val lastPathComponent = selectionPath!!.lastPathComponent + + if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) { + BranchChooserComboBoxDialog(project).show() + } + if (!capturedSmartReloadMode && capturedNavigateToSourceEnabled && lastPathComponent is NavigatableToSourceTreeNode @@ -382,8 +391,8 @@ class SnykToolWindowPanel( ApplicationManager.getApplication().invokeLater { doCleanUi(true) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } private fun doCleanUi(reDisplayDescription: Boolean) { @@ -440,7 +449,7 @@ class SnykToolWindowPanel( } private fun triggerScan() { - getSnykTaskQueueService(project)?.scan(false) + getSnykTaskQueueService(project)?.scan() } fun displayAuthPanel() { @@ -883,6 +892,7 @@ class SnykToolWindowPanel( selectedNodeUserObject: Any?, ) { val selectedNode = TreeUtil.findNodeWithObject(rootTreeNode, selectedNodeUserObject) + if (selectedNode is InfoTreeNode) return displayEmptyDescription() (vulnerabilitiesTree.model as DefaultTreeModel).reload(nodeToReload) @@ -910,12 +920,13 @@ class SnykToolWindowPanel( private fun selectAndDisplayNodeWithIssueDescription(selectCondition: (DefaultMutableTreeNode) -> Boolean) { val node = TreeUtil.findNode(rootTreeNode) { selectCondition(it) } if (node != null) { - navigateToSourceEnabled = false - try { - TreeUtil.selectNode(vulnerabilitiesTree, node) - // here TreeSelectionListener is invoked, so no needs for explicit updateDescriptionPanelBySelectedTreeNode() - } finally { - navigateToSourceEnabled = true + invokeLater { + try { + triggerSelectionListeners = false + TreeUtil.selectNode(vulnerabilitiesTree, node) + } finally { + triggerSelectionListeners = true + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt index 73cf445d0..ae02db22e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.TextRange @@ -19,16 +20,19 @@ import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_ import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NODE_NOT_SUPPORTED_STATE import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NO_OSS_FILES import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.OSS_ROOT_TEXT +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.SCANNING_TEXT import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode import snyk.common.ProductType import snyk.common.SnykFileIssueComparator +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import javax.swing.JTree @@ -70,25 +74,25 @@ class SnykToolWindowSnykScanListenerLS( ApplicationManager.getApplication().invokeLater { this.rootSecurityIssuesTreeNode.userObject = "$CODE_SECURITY_ROOT_TEXT (scanning finished)" this.rootQualityIssuesTreeNode.userObject = "$CODE_QUALITY_ROOT_TEXT (scanning finished)" - this.snykToolWindowPanel.navigateToSourceEnabled = false + this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displaySnykCodeResults(snykCachedResults?.currentSnykCodeResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) - this.snykToolWindowPanel.navigateToSourceEnabled = true + this.snykToolWindowPanel.triggerSelectionListeners = true } + refreshAnnotationsForOpenFiles(project) } override fun scanningOssFinished() { if (disposed) return + cancelOssIndicator(project) ApplicationManager.getApplication().invokeLater { - cancelOssIndicator(project) this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (scanning finished)" - this.snykToolWindowPanel.navigateToSourceEnabled = false + this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displayOssResults(snykCachedResults?.currentOSSResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) - this.snykToolWindowPanel.navigateToSourceEnabled = true + this.snykToolWindowPanel.triggerSelectionListeners = true } + refreshAnnotationsForOpenFiles(project) } override fun scanningError(snykScan: SnykScanParams) { @@ -113,10 +117,10 @@ class SnykToolWindowSnykScanListenerLS( // TODO implement } } + refreshAnnotationsForOpenFiles(project) } - override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) { - } + override fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) {} fun displaySnykCodeResults(snykResults: Map>) { if (disposed) return @@ -167,6 +171,7 @@ class SnykToolWindowSnykScanListenerLS( snykResults = snykResults, rootNode = this.rootOssIssuesTreeNode, ossResultsCount = snykResults.values.flatten().distinct().size, + fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable } ) } @@ -204,7 +209,6 @@ class SnykToolWindowSnykScanListenerLS( addInfoTreeNodes( rootNode = rootNode, issues = snykResults.values.flatten().distinct(), - securityIssuesCount = securityIssuesCount, fixableIssuesCount = fixableIssuesCount, ) @@ -259,32 +263,38 @@ class SnykToolWindowSnykScanListenerLS( fun addInfoTreeNodes( rootNode: DefaultMutableTreeNode, issues: List, - securityIssuesCount: Int? = null, fixableIssuesCount: Int? = null, ) { if (disposed) return - - // only add these info tree nodes to Snyk Code security vulnerabilities for now - if (securityIssuesCount == null) { + if (rootNode.userObject == SCANNING_TEXT) { return } val settings = pluginSettings() - // only add these when we enable the consistent ignores flow for now - if (!settings.isGlobalIgnoresFeatureEnabled) { - return + if (settings.isDeltaFindingsEnabled()) { + // we need one choose branch node for each content root. sigh. + service().getAllForProject(project).forEach { + val branchChooserTreeNode = ChooseBranchNode( + project = project, + info = "Click to choose base branch for ${it.folderPath} [ current: ${it.baseBranch} ]" + ) + rootNode.add(branchChooserTreeNode) + } } var text = "✅ Congrats! No vulnerabilities found!" val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } if (issuesCount != 0) { - if (issuesCount == 1) { - text = "$issuesCount vulnerability found by Snyk" + val plural = if (issuesCount == 1) { + "y" } else { - text = "✋ $issuesCount vulnerabilities found by Snyk" + "ies" + } + text = "✋ $issuesCount vulnerabilit$plural found by Snyk" + if (pluginSettings().isGlobalIgnoresFeatureEnabled) { + text += ", $ignoredIssuesCount ignored" } - text += ", $ignoredIssuesCount ignored" } rootNode.add( InfoTreeNode( @@ -297,31 +307,32 @@ class SnykToolWindowSnykScanListenerLS( if (fixableIssuesCount > 0) { rootNode.add( InfoTreeNode( - "⚡ $fixableIssuesCount vulnerabilities can be fixed by Snyk DeepCode AI", + "⚡ $fixableIssuesCount vulnerabilities can be fixed automatically", project, ), ) } else { rootNode.add( - InfoTreeNode("There are no vulnerabilities fixable by Snyk DeepCode AI", project), + InfoTreeNode("There are no vulnerabilities automatically fixable", project), ) } } - - if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) { - rootNode.add( - InfoTreeNode( - "Adjust your Issue View Options to see ignored issues.", - project, - ), - ) - } else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) { - rootNode.add( - InfoTreeNode( - "Adjust your Issue View Options to open issues.", - project, - ), - ) + if (pluginSettings().isGlobalIgnoresFeatureEnabled) { + if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) { + rootNode.add( + InfoTreeNode( + "Adjust your Issue View Options to see ignored issues.", + project, + ), + ) + } else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) { + rootNode.add( + InfoTreeNode( + "Adjust your Issue View Options to open issues.", + project, + ), + ) + } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt index 386c0a634..05dd3acd9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -18,6 +18,7 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode +import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode @@ -139,6 +140,11 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { nodeIcon = AllIcons.General.Error } + is ChooseBranchNode -> { + text = value.info + nodeIcon = value.icon + } + is InfoTreeNode -> { val info = value.userObject as String text = info diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt index c9fadd6a7..4a0516029 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/nodes/secondlevel/InfoTreeNode.kt @@ -1,9 +1,14 @@ package io.snyk.plugin.ui.toolwindow.nodes.secondlevel +import com.intellij.icons.AllIcons import com.intellij.openapi.project.Project import javax.swing.tree.DefaultMutableTreeNode -class InfoTreeNode( - private val info: String, - val project: Project, +open class InfoTreeNode( + open val info: String, + open val project: Project, ) : DefaultMutableTreeNode(info) + +class ChooseBranchNode(override val info: String = "Choose base branch", override val project: Project) : InfoTreeNode(info, project) { + val icon = AllIcons.Vcs.BranchNode +} diff --git a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt deleted file mode 100644 index 322bb70ed..000000000 --- a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt +++ /dev/null @@ -1,181 +0,0 @@ -package snyk.code.annotator - -import com.intellij.codeInsight.intention.IntentionAction -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.ExternalAnnotator -import com.intellij.lang.annotation.HighlightSeverity -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile -import io.snyk.plugin.getSnykCachedResultsForProduct -import io.snyk.plugin.toLanguageServerURL -import org.eclipse.lsp4j.CodeActionContext -import org.eclipse.lsp4j.CodeActionParams -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.TextDocumentIdentifier -import snyk.code.annotator.SnykAnnotator.SnykAnnotation -import snyk.common.AnnotatorCommon -import snyk.common.ProductType -import snyk.common.lsp.LanguageServerWrapper -import snyk.common.lsp.ScanIssue -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -private const val CODEACTION_TIMEOUT = 5000L - -abstract class SnykAnnotator(private val product: ProductType) : - ExternalAnnotator>, List>(), Disposable { - val logger = logger() - protected var disposed = false - get() { - return ApplicationManager.getApplication().isDisposed || field - } - - fun isDisposed() = disposed - - override fun dispose() { - disposed = true - } - - inner class SnykAnnotation( - val annotationSeverity: HighlightSeverity, - val annotationMessage: String, - val range: TextRange, - val intention: IntentionAction - ) - - // overrides needed for the Annotator to invoke apply(). We don't do anything here - override fun collectInformation(file: PsiFile): Pair> { - return Pair(file, getIssuesForFile(file) - .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } - .distinctBy { it.id } - .sortedBy { it.title }) - } - - override fun doAnnotate(initial: Pair>): List { - if (disposed) return emptyList() - AnnotatorCommon.prepareAnnotate(initial.first) - if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() - - val annotations = mutableListOf() - initial.second.forEach { issue -> - val textRange = textRange(initial.first, issue.range) - val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() - val annotationMessage = issue.annotationMessage() - if (textRange == null) { - logger.warn("Invalid range for issue: $issue") - return@forEach - } - if (!textRange.isEmpty) { - annotations.add( - SnykAnnotation( - highlightSeverity, - annotationMessage, - textRange, - ShowDetailsIntentionAction(annotationMessage, issue) - ) - ) - val params = - CodeActionParams( - TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), - issue.range, - CodeActionContext(emptyList()), - ) - val languageServer = LanguageServerWrapper.getInstance().languageServer - val codeActions = - try { - languageServer.textDocumentService - .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() - } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code actions for issue: $issue") - emptyList() - } - - codeActions - .filter { a -> - val diagnosticCode = a.right.diagnostics?.get(0)?.code?.left - val ruleId = issue.ruleId() - diagnosticCode == ruleId - } - .sortedBy { it.right.title }.forEach { action -> - val codeAction = action.right - val title = codeAction.title - annotations.add( - SnykAnnotation( - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) - ) - } - - } - } - return annotations - } - - override fun apply( - psiFile: PsiFile, - annotationResult: List, - holder: AnnotationHolder, - ) { - if (disposed) return - if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.forEach { annotation -> - if (!annotation.range.isEmpty) { - holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) - .range(annotation.range) - .withFix(annotation.intention) - .create() - } - } - } - - private fun getIssuesForFile(psiFile: PsiFile): Set = - getSnykCachedResultsForProduct(psiFile.project, product) - ?.filter { it.key.virtualFile == psiFile.virtualFile } - ?.map { it.value } - ?.flatten() - ?.toSet() - ?: emptySet() - - /** Public for Tests only */ - fun textRange( - psiFile: PsiFile, - range: Range, - ): TextRange? { - try { - val document = - psiFile.viewProvider.document ?: throw IllegalArgumentException("No document found for $psiFile") - val startRow = range.start.line - val endRow = range.end.line - val startCol = range.start.character - val endCol = range.end.character - - if (startRow < 0 || startRow > document.lineCount - 1) { - return null - } - if (endRow < 0 || endRow > document.lineCount - 1 || endRow < startRow) { - return null - } - - val lineOffSet = document.getLineStartOffset(startRow) + startCol - val lineOffSetEnd = document.getLineStartOffset(endRow) + endCol - - if (lineOffSet < 0 || lineOffSet > document.textLength - 1) { - return null - } - if (lineOffSetEnd < 0 || lineOffSetEnd < lineOffSet || lineOffSetEnd > document.textLength - 1) { - return null - } - - return TextRange.create(lineOffSet, lineOffSetEnd) - } catch (e: IllegalArgumentException) { - logger.warn(e) - return TextRange.EMPTY_RANGE - } - } -} diff --git a/src/main/kotlin/snyk/common/AnnotatorCommon.kt b/src/main/kotlin/snyk/common/AnnotatorCommon.kt index 8f2747bb7..ac5d6fc81 100644 --- a/src/main/kotlin/snyk/common/AnnotatorCommon.kt +++ b/src/main/kotlin/snyk/common/AnnotatorCommon.kt @@ -1,7 +1,6 @@ package snyk.common import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.psi.PsiFile @@ -37,7 +36,7 @@ object AnnotatorCommon { SnykProductsOrSeverityListener.SNYK_ENABLEMENT_TOPIC, object : SnykProductsOrSeverityListener { override fun enablementChanged() { - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } ) @@ -46,7 +45,7 @@ object AnnotatorCommon { SnykSettingsListener.SNYK_SETTINGS_TOPIC, object : SnykSettingsListener { override fun settingsChanged() { - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } ) diff --git a/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt similarity index 95% rename from src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt rename to src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt index 9a1f8f9a2..e230e8155 100644 --- a/src/main/kotlin/snyk/code/annotator/CodeActionIntention.kt +++ b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.codeInsight.intention.PriorityAction import com.intellij.openapi.command.WriteCommandAction @@ -20,14 +20,13 @@ import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import javax.swing.Icon -private const val TIMEOUT = 120L - class CodeActionIntention( private val issue: ScanIssue, private val codeAction: CodeAction, private val product: ProductType, ) : SnykIntentionActionBase() { private var changes: Map>? = null + private val timeout = 120L override fun getText(): String = codeAction.title @@ -39,7 +38,7 @@ class CodeActionIntention( if (codeAction.command == null && codeAction.edit == null) { resolvedCodeAction = languageServer.textDocumentService - .resolveCodeAction(codeAction).get(TIMEOUT, TimeUnit.SECONDS) + .resolveCodeAction(codeAction).get(timeout, TimeUnit.SECONDS) val edit = resolvedCodeAction.edit if (edit == null || edit.changes == null) return @@ -49,7 +48,7 @@ class CodeActionIntention( val executeCommandParams = ExecuteCommandParams(codeActionCommand.command, codeActionCommand.arguments) languageServer.workspaceService - .executeCommand(executeCommandParams).get(TIMEOUT, TimeUnit.SECONDS) + .executeCommand(executeCommandParams).get(timeout, TimeUnit.SECONDS) } } diff --git a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt new file mode 100644 index 000000000..bd44321b5 --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt @@ -0,0 +1,55 @@ +package snyk.common.annotator + +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter +import com.intellij.openapi.fileTypes.SyntaxHighlighter +import com.intellij.openapi.options.colors.AttributesDescriptor +import com.intellij.openapi.options.colors.ColorDescriptor +import com.intellij.openapi.options.colors.ColorSettingsPage +import icons.SnykIcons +import javax.swing.Icon + +object SnykAnnotationAttributeKey { + val unknown: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Unknown Severity", TextAttributesKey.find("INFO_ATTRIBUTES")) + val low: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Low Severity", TextAttributesKey.find("INFO_ATTRIBUTES")) + val medium: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Medium Severity", TextAttributesKey.find("WARNING_ATTRIBUTES")) + val high: TextAttributesKey = TextAttributesKey.createTextAttributesKey("High Severity", TextAttributesKey.find("ERRORS_ATTRIBUTES")) + val critical: TextAttributesKey = TextAttributesKey.createTextAttributesKey("Critical Severity", TextAttributesKey.find("ERRORS_ATTRIBUTES")) +} + +class SnykAnnotationColorSettingsPage : ColorSettingsPage { + + private val attributesDescriptors = arrayOf( + AttributesDescriptor("Snyk Critical Issue", SnykAnnotationAttributeKey.critical), + AttributesDescriptor("Snyk High Issue", SnykAnnotationAttributeKey.high), + AttributesDescriptor("Snyk Medium Issue", SnykAnnotationAttributeKey.medium), + AttributesDescriptor("Snyk Low Issue", SnykAnnotationAttributeKey.low), + AttributesDescriptor("Snyk Unknown Issue", SnykAnnotationAttributeKey.unknown), + ) + + override fun getIcon(): Icon = SnykIcons.TOOL_WINDOW + + override fun getHighlighter(): SyntaxHighlighter = PlainSyntaxHighlighter() + + override fun getDemoText(): String = + "This is a demo of a Snyk Critical Severity Issue\n" + + "This is a demo of a Snyk High Severity Issue\n" + + "This is a demo of a Snyk Medium Severity Issue\n" + + "This is a demo of a Snyk Low Severity Issue\n" + + "This is a demo of a Snyk Unknown Severity Issue\n" + + override fun getAdditionalHighlightingTagToDescriptorMap(): Map = + mapOf( + "snyk_unknown_issue" to SnykAnnotationAttributeKey.unknown, + "snyk_low_issue" to SnykAnnotationAttributeKey.low, + "snyk_medium_issue" to SnykAnnotationAttributeKey.medium, + "snyk_high_issue" to SnykAnnotationAttributeKey.high, + "snyk_critical_issue" to SnykAnnotationAttributeKey.critical + ) + + override fun getAttributeDescriptors(): Array = attributesDescriptors + + override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY + + override fun getDisplayName(): String = "Snyk Colors" +} diff --git a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt b/src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt similarity index 89% rename from src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt rename to src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt index 3f88f90df..5fce33046 100644 --- a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt +++ b/src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import io.snyk.plugin.Severity import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel @@ -7,7 +7,7 @@ import snyk.common.lsp.ScanIssue class ShowDetailsIntentionAction( override val annotationMessage: String, - private val issue: ScanIssue + val issue: ScanIssue ) : ShowDetailsIntentionActionBase() { override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { toolWindowPanel.selectNodeAndDisplayDescription(issue) diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt new file mode 100644 index 000000000..f482120a5 --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -0,0 +1,303 @@ +package snyk.common.annotator + +import com.intellij.codeInsight.daemon.LineMarkerProviders +import com.intellij.codeInsight.daemon.LineMarkerSettings +import com.intellij.codeInsight.intention.IntentionAction +import com.intellij.lang.Language.ANY +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.ExternalAnnotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.guessProjectForFile +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiFile +import icons.SnykIcons +import io.snyk.plugin.Severity +import io.snyk.plugin.getSnykCachedResultsForProduct +import io.snyk.plugin.getSnykToolWindowPanel +import io.snyk.plugin.toLanguageServerURL +import org.eclipse.lsp4j.CodeAction +import org.eclipse.lsp4j.CodeActionContext +import org.eclipse.lsp4j.CodeActionParams +import org.eclipse.lsp4j.Command +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.jetbrains.concurrency.runAsync +import snyk.common.AnnotatorCommon +import snyk.common.ProductType +import snyk.common.annotator.SnykAnnotationAttributeKey.critical +import snyk.common.annotator.SnykAnnotationAttributeKey.high +import snyk.common.annotator.SnykAnnotationAttributeKey.low +import snyk.common.annotator.SnykAnnotationAttributeKey.medium +import snyk.common.annotator.SnykAnnotationAttributeKey.unknown +import snyk.common.annotator.SnykAnnotator.SnykAnnotation +import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.RangeConverter +import snyk.common.lsp.ScanIssue +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import javax.swing.Icon + + +private const val CODEACTION_TIMEOUT = 10L + +typealias SnykAnnotationInput = Pair>> +typealias SnykAnnotationList = List + +abstract class SnykAnnotator(private val product: ProductType) : + ExternalAnnotator(), Disposable, DumbAware { + private val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() + + val logger = logger() + protected var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + + override fun dispose() { + disposed = true + } + + inner class SnykAnnotation( + val issue: ScanIssue, + val annotationSeverity: HighlightSeverity, + val annotationMessage: String, + val range: TextRange, + val intentionActions: MutableList = mutableListOf(), + var gutterIconRenderer: GutterIconRenderer? = null + ) + + override fun collectInformation(file: PsiFile): SnykAnnotationInput? { + if (disposed) return null + if (!LanguageServerWrapper.getInstance().isInitialized) return null + val map = getIssuesForFile(file) + .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } + .sortedByDescending { it.getSeverityAsEnum() } + .groupBy { it.range } + .toMap() + + return Pair(file, map) + } + + override fun doAnnotate(initialInfo: SnykAnnotationInput): SnykAnnotationList { + if (disposed) return emptyList() + if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() + + val psiFile = initialInfo.first + val gutterIconEnabled = LineMarkerSettings.getSettings().isEnabled(lineMarkerProviderDescriptor) + + AnnotatorCommon.prepareAnnotate(psiFile) + + val codeActions = initialInfo.second + .map { entry -> + entry.key to getCodeActions(psiFile.virtualFile, entry.key).map { it.right } + .sortedBy { it.title } + }.toMap() + + val annotations = mutableListOf() + initialInfo.second.forEach { entry -> + val textRange = RangeConverter.convertToTextRange(psiFile, entry.key) + if (textRange == null || textRange.isEmpty) { + logger.warn("Invalid range for range: $textRange") + return@forEach + } + annotations.addAll(doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions)) + } + return annotations.sortedByDescending { it.issue.getSeverityAsEnum() } + } + + private fun doAnnotateIssue( + entry: Map.Entry>, + textRange: TextRange, + gutterIconEnabled: Boolean, + codeActions: Map>, + ): List { + val gutterIcons: MutableSet = mutableSetOf() + val annotations = mutableListOf() + entry.value.forEach { issue -> + val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() + val annotationMessage = issue.annotationMessage() + + val detailAnnotation = SnykAnnotation( + issue, + highlightSeverity, + annotationMessage, + textRange, + ) + + val gutterIconRenderer = + if (gutterIconEnabled && !gutterIcons.contains(textRange)) { + gutterIcons.add(textRange) + SnykShowDetailsGutterRenderer(detailAnnotation) + } else { + null + } + + val languageServerIntentionActions = codeActions[entry.key]?.let { range -> + getCodeActionsAsIntentionActions(issue, range) + } ?: emptyList() + + detailAnnotation.gutterIconRenderer = gutterIconRenderer + detailAnnotation.intentionActions.add(ShowDetailsIntentionAction(annotationMessage, issue)) + detailAnnotation.intentionActions.addAll(languageServerIntentionActions) + annotations.add(detailAnnotation) + } + return annotations + } + + override fun apply( + psiFile: PsiFile, + annotationResult: SnykAnnotationList, + holder: AnnotationHolder, + ) { + if (disposed) return + if (!LanguageServerWrapper.getInstance().isInitialized) return + annotationResult + .forEach { annotation -> + if (!annotation.range.isEmpty) { + val annoBuilder = holder + .newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + .range(annotation.range) + .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) + + annotation.intentionActions.forEach { + annoBuilder.withFix(it) + } + + if (annotation.gutterIconRenderer != null) { + annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + } + + annoBuilder.create() + } + } + } + + private fun getCodeActionsAsIntentionActions( + issue: ScanIssue, + codeActions: List + ): MutableList { + val addedIntentionActions = mutableListOf() + + codeActions + .filter { action -> + val diagnosticCode = action.diagnostics?.get(0)?.code?.left + val ruleId = issue.ruleId() + diagnosticCode == ruleId + } + .forEach { action -> + addedIntentionActions.add(CodeActionIntention(issue, action, product)) + } + + return addedIntentionActions + } + + private fun getCodeActions( + file: VirtualFile, + range: Range + ): List> { + val params = + CodeActionParams( + TextDocumentIdentifier(file.toLanguageServerURL()), + range, + CodeActionContext(emptyList()), + ) + val languageServer = LanguageServerWrapper.getInstance().languageServer + val codeActions = + try { + languageServer.textDocumentService + .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.SECONDS) ?: emptyList() + } catch (ignored: TimeoutException) { + logger.info("Timeout fetching code actions for range: $range") + emptyList() + } + return codeActions + } + + private fun getLineMarkerProvider(): SnykLineMarkerProvider { + val lineMarkerProviderDescriptor: SnykLineMarkerProvider = + LineMarkerProviders.getInstance().allForLanguage(ANY) + .stream() + .filter { p -> p is SnykLineMarkerProvider } + .findFirst() + .orElse(null) as SnykLineMarkerProvider + return lineMarkerProviderDescriptor + } + + private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { + return when (severity) { + Severity.UNKNOWN -> unknown + Severity.LOW -> low + Severity.MEDIUM -> medium + Severity.HIGH -> high + Severity.CRITICAL -> critical + } + } + + private fun getIssuesForFile(psiFile: PsiFile): Set = + getSnykCachedResultsForProduct(psiFile.project, product) + ?.filter { it.key.virtualFile == psiFile.virtualFile } + ?.map { it.value } + ?.flatten() + ?.toSet() + ?: emptySet() + + class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { + override fun equals(other: Any?): Boolean { + return annotation == other + } + + override fun hashCode(): Int { + return annotation.hashCode() + } + + override fun getIcon(): Icon { + return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) + } + + override fun getClickAction(): AnAction? { + val intention = + annotation.intentionActions.firstOrNull { it is ShowDetailsIntentionAction } ?: return null + return getShowDetailsNavigationAction(intention as ShowDetailsIntentionAction) + } + + private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = + object : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + runAsync { + val virtualFile = intention.issue.virtualFile ?: return@runAsync + val project = guessProjectForFile(virtualFile) ?: return@runAsync + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@runAsync + intention.selectNodeAndDisplayDescription(toolWindowPanel) + } + } + } + + override fun getTooltipText(): String { + return annotation.annotationMessage + } + + override fun getAccessibleName(): String { + return annotation.annotationMessage + } + + override fun isNavigateAction(): Boolean { + return true + } + + override fun isDumbAware(): Boolean { + return true + } + } +} diff --git a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt similarity index 95% rename from src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt index 2efd7d999..d99df7262 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykCodeAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykCodeAnnotator.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer diff --git a/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt b/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt new file mode 100644 index 000000000..37ee1b6d7 --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt @@ -0,0 +1,23 @@ +package snyk.common.annotator + +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor +import com.intellij.psi.PsiElement +import icons.SnykIcons +import javax.swing.Icon + +// we only define a line marker provider so we can use the gutter icon settings to switch +// rendering of gutter icons on and off +class SnykLineMarkerProvider : LineMarkerProviderDescriptor() { + override fun getName(): String { + return "Snyk Security" + } + + override fun getIcon(): Icon { + return SnykIcons.TOOL_WINDOW + } + + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { + return null + } +} diff --git a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt similarity index 96% rename from src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt index 8e9653183..a16cf220c 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykOSSAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykOSSAnnotator.kt @@ -1,4 +1,4 @@ -package snyk.code.annotator +package snyk.common.annotator import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer diff --git a/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt b/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt deleted file mode 100644 index 3ff1fa7e1..000000000 --- a/src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt +++ /dev/null @@ -1,45 +0,0 @@ -package snyk.common.intentionactions - -import com.intellij.codeInsight.intention.PriorityAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Iconable.IconFlags -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiFile -import com.intellij.util.DocumentUtil -import icons.SnykIcons -import io.snyk.plugin.refreshAnnotationsForOpenFiles -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import javax.swing.Icon - -class AlwaysAvailableReplacementIntentionAction( - val range: TextRange, - val replacementText: String, - val message: String = "" -) : SnykIntentionActionBase() { - - override fun getIcon(@IconFlags flags: Int): Icon = SnykIcons.CHECKMARK_GREEN - - override fun getPriority(): PriorityAction.Priority = PriorityAction.Priority.TOP - - override fun getText(): String = intentionTextPrefix + replacementText + intentionTextPostfix - - override fun invoke(project: Project, editor: Editor?, file: PsiFile) { - val doc = editor?.document ?: return - DocumentUtil.writeInRunUndoTransparentAction { - doc.replaceString(range.startOffset, range.endOffset, replacementText) - // save all changes on disk to update caches through SnykBulkFileListener - FileDocumentManager.getInstance().saveDocument(doc) - refreshAnnotationsForOpenFiles(project) - if (message.isNotBlank()) { - SnykBalloonNotificationHelper.showWarn(message, project) - } - } - } - - companion object { - private const val intentionTextPrefix = "Upgrade to " - private const val intentionTextPostfix = " (Snyk)" - } -} diff --git a/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt b/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt index fd2ad6f71..e0cadb66e 100644 --- a/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt +++ b/src/main/kotlin/snyk/common/intentionactions/SnykIntentionActionBase.kt @@ -7,7 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Iconable import com.intellij.psi.PsiFile -abstract class SnykIntentionActionBase() : IntentionAction, Iconable, PriorityAction { +abstract class SnykIntentionActionBase : IntentionAction, Iconable, PriorityAction { override fun startInWriteAction(): Boolean = true diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt index fc6621b26..bd9d56e1f 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt @@ -7,7 +7,10 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.SimplePersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage +import com.intellij.openapi.project.Project import com.intellij.util.xmlb.annotations.MapAnnotation +import io.snyk.plugin.getContentRootPaths +import java.util.stream.Collectors @Service @State( @@ -21,15 +24,27 @@ class FolderConfigSettings : SimplePersistentStateComponent() } - private fun addFolderConfig(folderConfig: FolderConfig) { + fun addFolderConfig(folderConfig: FolderConfig) { state.configs[folderConfig.folderPath] = gson.toJson(folderConfig) } - fun getFolderConfig(folderPath: String): FolderConfig? { - return gson.fromJson(state.configs[folderPath], FolderConfig::class.java) + @Suppress("USELESS_ELVIS", "SENSELESS_COMPARISON") // gson doesn't care about kotlin not null stuff + internal fun getFolderConfig(folderPath: String): FolderConfig? { + val fromJson = gson.fromJson(state.configs[folderPath], FolderConfig::class.java) ?: return null + if (fromJson.additionalParameters == null) { + val copy = fromJson.copy( + baseBranch = fromJson.baseBranch, + folderPath = fromJson.folderPath, + localBranches = fromJson.localBranches ?: emptyList(), + additionalParameters = fromJson.additionalParameters ?: emptyList(), + ) + addFolderConfig(copy) + return copy + } + return fromJson } - fun getAll() : Map { + fun getAll(): Map { return state.configs.map { it.key to gson.fromJson(it.value, FolderConfig::class.java) }.toMap() @@ -38,4 +53,11 @@ class FolderConfigSettings : SimplePersistentStateComponent) = folderConfigs.forEach { addFolderConfig(it) } + + fun getAllForProject(project: Project): List = + project.getContentRootPaths() + .mapNotNull { getFolderConfig(it.toAbsolutePath().toString()) } + .stream() + .sorted() + .collect(Collectors.toList()).toList() } diff --git a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt index 16518ecfd..610ccc4fa 100644 --- a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt @@ -16,6 +16,7 @@ import com.intellij.openapi.progress.Task.Backgroundable import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFile import icons.SnykIcons import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeLens @@ -26,7 +27,7 @@ import java.awt.event.MouseEvent import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -private const val CODELENS_FETCH_TIMEOUT = 2L +private const val CODELENS_FETCH_TIMEOUT = 10L @Suppress("UnstableApiUsage") class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingProvider { @@ -47,44 +48,42 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro } override fun computeCodeVision(editor: Editor, uiData: Unit): CodeVisionState { - if (editor.project == null) return CodeVisionState.READY_EMPTY + if (LanguageServerWrapper.getInstance().isDisposed()) return CodeVisionState.READY_EMPTY if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY + val project = editor.project ?: return CodeVisionState.READY_EMPTY - return ReadAction.compute { - val project = editor.project ?: return@compute CodeVisionState.READY_EMPTY - val document = editor.document - val file = PsiDocumentManager.getInstance(project).getPsiFile(document) - ?: return@compute CodeVisionState.READY_EMPTY - val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL())) - val lenses = mutableListOf>() - val codeLenses = try { - LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params) - .get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) - } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code lenses for : $file") - emptyList() - } + val document = editor.document - if (codeLenses == null) { - return@compute CodeVisionState.READY_EMPTY - } - codeLenses.forEach { codeLens -> - val range = TextRange( - document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character, - document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character - ) + val file = ReadAction.compute { + PsiDocumentManager.getInstance(project).getPsiFile(document) + } ?: return CodeVisionState.READY_EMPTY - val entry = ClickableTextCodeVisionEntry( - text = codeLens.command.title, - providerId = id, - onClick = LSCommandExecutionHandler(codeLens), - extraActions = emptyList(), - icon = SnykIcons.TOOL_WINDOW - ) - lenses.add(range to entry) - } - return@compute CodeVisionState.Ready(lenses) + val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL())) + val lenses = mutableListOf>() + val codeLenses = try { + LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params) + .get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) ?: return CodeVisionState.READY_EMPTY + } catch (ignored: TimeoutException) { + logger.info("Timeout fetching code lenses for : $file") + return CodeVisionState.READY_EMPTY + } + + codeLenses.forEach { codeLens -> + val range = TextRange( + document.getLineStartOffset(codeLens.range.start.line) + codeLens.range.start.character, + document.getLineEndOffset(codeLens.range.end.line) + codeLens.range.end.character + ) + + val entry = ClickableTextCodeVisionEntry( + text = codeLens.command.title, + providerId = id, + onClick = LSCommandExecutionHandler(codeLens), + extraActions = emptyList(), + icon = SnykIcons.TOOL_WINDOW + ) + lenses.add(range to entry) } + return CodeVisionState.Ready(lenses) } private class LSCommandExecutionHandler(private val codeLens: CodeLens) : (MouseEvent?, Editor) -> Unit { diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt new file mode 100644 index 000000000..66fb4ae47 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -0,0 +1,77 @@ +@file:Suppress("UnstableApiUsage") + +package snyk.common.lsp + +import com.intellij.markdown.utils.convertMarkdownToHtml +import com.intellij.model.Pointer +import com.intellij.openapi.Disposable +import com.intellij.platform.backend.documentation.DocumentationResult +import com.intellij.platform.backend.documentation.DocumentationTarget +import com.intellij.platform.backend.documentation.DocumentationTargetProvider +import com.intellij.platform.backend.presentation.TargetPresentation +import com.intellij.psi.PsiFile +import icons.SnykIcons +import io.snyk.plugin.isDocumentationHoverEnabled +import io.snyk.plugin.toLanguageServerURL +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.Hover +import org.eclipse.lsp4j.HoverParams +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.TextDocumentIdentifier +import java.util.concurrent.TimeUnit + +class SnykDocumentationTargetPointer(private val documentationTarget: DocumentationTarget) : + Pointer { + + override fun dereference(): DocumentationTarget { + return documentationTarget + } +} + +class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { + private var disposed = false + get() { + return SnykPluginDisposable.getInstance().isDisposed() || field + } + + fun isDisposed() = disposed + + override fun documentationTargets(file: PsiFile, offset: Int): MutableList { + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (disposed || !languageServerWrapper.isInitialized || !isDocumentationHoverEnabled()) return mutableListOf() + + val lineNumber = file.viewProvider.document.getLineNumber(offset) + val lineStartOffset = file.viewProvider.document.getLineStartOffset(lineNumber) + val hoverParams = HoverParams( + TextDocumentIdentifier(file.virtualFile.toLanguageServerURL()), + Position(lineNumber, offset - lineStartOffset) + ) + val hover = + languageServerWrapper.languageServer.textDocumentService.hover(hoverParams).get(2000, TimeUnit.MILLISECONDS) + if (hover == null || hover.contents.right.value.isEmpty()) return mutableListOf() + return mutableListOf(SnykDocumentationTarget(hover)) + } + + inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { + override fun computeDocumentation(): DocumentationResult? { + val htmlText = hover.contents.right.value + if (htmlText.isEmpty()) { + return null + } + return DocumentationResult.documentation(htmlText) + } + + override fun computePresentation(): TargetPresentation { + return TargetPresentation.builder("Snyk Security").icon(SnykIcons.TOOL_WINDOW).presentation() + } + + override fun createPointer(): Pointer { + return SnykDocumentationTargetPointer(this) + } + } + + + override fun dispose() { + disposed = true + } +} diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index 4445a112c..8b57c0b1d 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -3,6 +3,7 @@ package snyk.common.lsp import com.google.gson.annotations.SerializedName +import com.intellij.openapi.components.service import io.snyk.plugin.pluginSettings import org.apache.commons.lang3.SystemUtils import snyk.pluginInfo @@ -16,7 +17,7 @@ data class LanguageServerSettings( @SerializedName("additionalParams") val additionalParams: String? = null, @SerializedName("additionalEnv") val additionalEnv: String? = null, @SerializedName("path") val path: String? = null, - @SerializedName("sendErrorReports") val sendErrorReports: String? = "false", + @SerializedName("sendErrorReports") val sendErrorReports: String? = "true", @SerializedName("organization") val organization: String? = null, @SerializedName("enableTelemetry") val enableTelemetry: String? = "false", @SerializedName("manageBinariesAutomatically") val manageBinariesAutomatically: String? = "false", @@ -42,6 +43,10 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), + @SerializedName("hoverVerbosity") val hoverVerbosity: Int = 1, + @SerializedName("outputFormat") val outputFormat: String = "html", + @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled().toString(), + @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) data class SeverityFilter( diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 1bbb72f05..8a414d228 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -43,6 +43,7 @@ import org.eclipse.lsp4j.WorkspaceEditCapabilities import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent import org.eclipse.lsp4j.jsonrpc.Launcher +import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.LanguageServer import org.jetbrains.concurrency.runAsync @@ -64,10 +65,10 @@ import snyk.trust.WorkspaceTrustService import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.ExecutorService import java.util.concurrent.Executors -import java.util.concurrent.Future import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.locks.ReentrantLock +import java.util.logging.Logger.getLogger import kotlin.io.path.exists private const val INITIALIZATION_TIMEOUT = 20L @@ -133,25 +134,35 @@ class LanguageServerWrapper( val cmd = listOf(lsPath, "language-server", "-l", logLevel) val processBuilder = ProcessBuilder(cmd) - pluginSettings().token?.let { EnvironmentHelper.updateEnvironment(processBuilder.environment(), it) } + EnvironmentHelper.updateEnvironment(processBuilder.environment(), pluginSettings().token ?: "") process = processBuilder.start() - launcher = LSPLauncher.createClientLauncher(languageClient, process.inputStream, process.outputStream) - languageServer = launcher.remoteProxy - GlobalScope.launch { if (!disposed) { try { - process.errorStream.bufferedReader().forEachLine { println(it) } - } catch (ignored: RuntimeException) { + process.errorStream.bufferedReader().forEachLine { logger.debug(it) } + } catch (ignored: Exception) { // ignore } } } - launcher.startListening() - sendInitializeMessage() - isInitialized = true + launcher = LSPLauncher.createClientLauncher(languageClient, process.inputStream, process.outputStream) + languageServer = launcher.remoteProxy + + val listenerFuture = launcher.startListening() + + runAsync { + listenerFuture.get() + isInitialized = false + } + + if (!listenerFuture.isDone) { + sendInitializeMessage() + isInitialized = true + } else { + logger.warn("Language Server initialization did not succeed") + } } catch (e: Exception) { logger.warn(e) process.destroy() @@ -159,16 +170,29 @@ class LanguageServerWrapper( } } - fun shutdown(): Future<*> = - executorService.submit { - if (::process.isInitialized && process.isAlive) { - languageServer.shutdown().get(1, TimeUnit.SECONDS) - languageServer.exit() - if (process.isAlive) { - process.destroyForcibly() - } + fun shutdown() { + // LSP4j logs errors and rethrows - this is bad practice, and we don't need that log here, so we shut it up. + val lsp4jLogger = getLogger(RemoteEndpoint::class.java.name) + val previousLSP4jLogLevel = lsp4jLogger.level + lsp4jLogger.level = java.util.logging.Level.OFF + try { + val shouldShutdown = lsIsAlive() + executorService.submit { if (shouldShutdown) languageServer.shutdown().get(1, TimeUnit.SECONDS) } + } catch (ignored: Exception) { + // we don't care + } finally { + try { + if (lsIsAlive()) languageServer.exit() + } catch (ignore: Exception) { + // do nothing + } finally { + if (lsIsAlive()) process.destroyForcibly() } + lsp4jLogger.level = previousLSP4jLogLevel } + } + + private fun lsIsAlive() = ::process.isInitialized && process.isAlive private fun determineWorkspaceFolders(): List { val workspaceFolders = mutableSetOf() @@ -597,8 +621,13 @@ class LanguageServerWrapper( return "" } - companion object { + override fun dispose() { + disposed = true + shutdown() + } + + companion object { private var instance: LanguageServerWrapper? = null fun getInstance() = instance ?: LanguageServerWrapper().also { @@ -607,10 +636,5 @@ class LanguageServerWrapper( } } - override fun dispose() { - disposed = true - shutdown() - } - } diff --git a/src/main/kotlin/snyk/common/lsp/RangeConverter.kt b/src/main/kotlin/snyk/common/lsp/RangeConverter.kt new file mode 100644 index 000000000..216606fe9 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/RangeConverter.kt @@ -0,0 +1,48 @@ +package snyk.common.lsp + +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiFile +import org.eclipse.lsp4j.Range + +class RangeConverter { + companion object { + val logger = logger() + /** Public for Tests only */ + fun convertToTextRange( + psiFile: PsiFile, + range: Range, + ): TextRange? { + try { + val document = + psiFile.viewProvider.document ?: throw IllegalArgumentException("No document found for $psiFile") + val startRow = range.start.line + val endRow = range.end.line + val startCol = range.start.character + val endCol = range.end.character + + if (startRow < 0 || startRow > document.lineCount - 1) { + return null + } + if (endRow < 0 || endRow > document.lineCount - 1 || endRow < startRow) { + return null + } + + val lineOffSet = document.getLineStartOffset(startRow) + startCol + val lineOffSetEnd = document.getLineStartOffset(endRow) + endCol + + if (lineOffSet < 0 || lineOffSet > document.textLength - 1) { + return null + } + if (lineOffSetEnd < 0 || lineOffSetEnd < lineOffSet || lineOffSetEnd > document.textLength - 1) { + return null + } + + return TextRange.create(lineOffSet, lineOffSetEnd) + } catch (e: IllegalArgumentException) { + logger.warn(e) + return TextRange.EMPTY_RANGE + } + } + } +} diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 03e609dc7..b6c99ba97 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -22,7 +22,6 @@ import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VfsUtilCore -import com.intellij.openapi.vfs.VirtualFileManager import io.snyk.plugin.SnykFile import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getContentRootVirtualFiles @@ -186,19 +185,17 @@ class SnykLanguageClient : private fun refreshUI(): CompletableFuture { val completedFuture: CompletableFuture = CompletableFuture.completedFuture(null) if (disposed) return completedFuture - - ProjectManager - .getInstance() - .openProjects - .filter { !it.isDisposed } - .forEach { project -> - runAsync { + runAsync { + ProjectManager + .getInstance() + .openProjects + .filter { !it.isDisposed } + .forEach { project -> ReadAction.run { if (!project.isDisposed) refreshAnnotationsForOpenFiles(project) } } - } - VirtualFileManager.getInstance().asyncRefresh() + } return completedFuture } @@ -304,6 +301,7 @@ class SnykLanguageClient : if (disposed) return if (pluginSettings().token == param.token) return pluginSettings().token = param.token + ApplicationManager.getApplication().saveSettings() if (pluginSettings().token?.isNotEmpty() == true && pluginSettings().scanOnSave) { val wrapper = LanguageServerWrapper.getInstance() @@ -462,12 +460,10 @@ class SnykLanguageClient : override fun showMessage(messageParams: MessageParams?) { if (disposed) return val project = ProjectUtil.getActiveProject() - if (project == null) { - logger.info(messageParams?.message) - return - } when (messageParams?.type) { - MessageType.Error -> SnykBalloonNotificationHelper.showError(messageParams.message, project) + MessageType.Error -> { + SnykBalloonNotificationHelper.showError(messageParams.message, project) + } MessageType.Warning -> SnykBalloonNotificationHelper.showWarn(messageParams.message, project) MessageType.Info -> { val notification = SnykBalloonNotificationHelper.showInfo(messageParams.message, project) diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 3553a3305..7a1f5a97d 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -285,11 +285,11 @@ data class ScanIssue( fun hasAIFix(): Boolean { return when (this.additionalData.getProductType()) { ProductType.OSS -> - return this.additionalData.isUpgradable == true + this.additionalData.isUpgradable + ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> { - return this.additionalData.hasAIFix + this.additionalData.hasAIFix } - else -> TODO() } } @@ -622,5 +622,6 @@ data class FolderConfigsParam( data class FolderConfig( @SerializedName("folderPath") val folderPath: String, @SerializedName("baseBranch") val baseBranch: String, - @SerializedName("localBranches") val localBranches: List = emptyList() + @SerializedName("localBranches") val localBranches: List = emptyList(), + @SerializedName("additionalParameters") val additionalParameters: List = emptyList() ) diff --git a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt index ac28a3f7e..6b80f37e3 100644 --- a/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt +++ b/src/main/kotlin/snyk/container/ContainerBulkFileListener.kt @@ -76,8 +76,8 @@ class ContainerBulkFileListener : SnykBulkFileListener() { snykCachedResults.currentContainerResult = newContainerCache ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayContainerResults(newContainerCache) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } private fun makeObsolete(containerIssuesForImage: ContainerIssuesForImage): ContainerIssuesForImage = diff --git a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt index 20545ff09..be1f02083 100644 --- a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt +++ b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt @@ -49,14 +49,15 @@ class IacBulkFileListener : SnykBulkFileListener() { } .forEach(::markObsolete) - val changed = iacRelatedVFsAffected.isNotEmpty() // for new/deleted/renamed files we also need to "dirty" the cache, too + val changed = + iacRelatedVFsAffected.isNotEmpty() // for new/deleted/renamed files we also need to "dirty" the cache, too if (changed) { log.debug("update IaC cache for $iacRelatedVFsAffected") currentIacResult.iacScanNeeded = true ApplicationManager.getApplication().invokeLater { getSnykToolWindowPanel(project)?.displayIacResults(currentIacResult) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } } diff --git a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt index baac7f5a3..217ce8601 100644 --- a/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt +++ b/src/main/kotlin/snyk/iac/IgnoreButtonActionListener.kt @@ -1,6 +1,5 @@ package snyk.iac -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task @@ -51,9 +50,7 @@ class IgnoreButtonActionListener( isEnabled = false text = IGNORED_ISSUE_BUTTON_TEXT } - ApplicationManager.getApplication().invokeLater { - refreshAnnotationsForOpenFiles(project) - } + refreshAnnotationsForOpenFiles(project) } catch (e: IgnoreException) { SnykBalloonNotificationHelper.showError( "Ignoring did not succeed. Error message: ${e.message})", project diff --git a/src/main/kotlin/snyk/net/HttpClient.kt b/src/main/kotlin/snyk/net/HttpClient.kt deleted file mode 100644 index 0559b48d6..000000000 --- a/src/main/kotlin/snyk/net/HttpClient.kt +++ /dev/null @@ -1,82 +0,0 @@ -package snyk.net - -import com.intellij.ide.BrowserUtil -import com.intellij.notification.NotificationAction -import io.snyk.plugin.getSSLContext -import io.snyk.plugin.getX509TrustManager -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import okhttp3.Interceptor -import okhttp3.OkHttpClient -import java.security.SecureRandom -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -/** - * An HTTP client. - * - * An `HttpClient` can be used in API clients based on Retrofit. It can be used - * to configure per-client state, like: a proxy, an authenticator, etc. - * - * Default timeout values: - * - _connect_ - 30 seconds - * - _read_ - 60 seconds - * - _write_ - 60 seconds - */ -class HttpClient( - var connectTimeout: Long = 30, - var readTimeout: Long = 60, - var writeTimeout: Long = 60, - private var disableSslVerification: Boolean = false, - var interceptors: List = listOf() -) { - companion object { - const val BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION = - "Could not initialize SSL security to communicate with the Snyk services (%s)." - } - - fun build(): OkHttpClient { - try { - val httpClientBuilder = OkHttpClient.Builder() - .connectTimeout(connectTimeout, TimeUnit.SECONDS) - .readTimeout(readTimeout, TimeUnit.SECONDS) - .writeTimeout(writeTimeout, TimeUnit.SECONDS) - .sslSocketFactory(getSSLContext().socketFactory, getX509TrustManager()) - httpClientBuilder.interceptors().addAll(interceptors) - - if (disableSslVerification) { - httpClientBuilder.ignoreAllSslErrors() - } - - return httpClientBuilder.build() - } catch (e: IllegalStateException) { - val message = String.format(BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION, e.localizedMessage) - SnykBalloonNotificationHelper.showError( - message, - null, - NotificationAction.createSimple("Contact support...") { - BrowserUtil.browse("https://snyk.io/contact-us/?utm_source=JETBRAINS_IDE") - }) - throw e - } - } -} - -private fun OkHttpClient.Builder.ignoreAllSslErrors() { - val unsafeTrustManager = object : X509TrustManager { - override fun checkClientTrusted(certs: Array, authType: String) = Unit - override fun checkServerTrusted(certs: Array, authType: String) = Unit - override fun getAcceptedIssuers(): Array = arrayOf() - } - - val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply { - val trustAllCertificates = arrayOf(unsafeTrustManager) - init(null, trustAllCertificates, SecureRandom()) - }.socketFactory - - sslSocketFactory(insecureSocketFactory, unsafeTrustManager) - hostnameVerifier(HostnameVerifier { _, _ -> true }) -} diff --git a/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt b/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt deleted file mode 100644 index d2e162f6b..000000000 --- a/src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt +++ /dev/null @@ -1,32 +0,0 @@ -package snyk.net - -import com.intellij.openapi.diagnostic.Logger -import okhttp3.Interceptor -import okhttp3.Response -import okhttp3.internal.closeQuietly -import okio.Buffer - -class HttpLoggingInterceptor(private val log: Logger) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - val buffer = Buffer() - val requestBody = request.body - requestBody?.writeTo(buffer) - log.warn("--> ${request.method} ${request.url}, payload=${buffer.readUtf8()}") - - val response = chain.proceed(request) - val responseBody = response.body - val responseStr = responseBody?.string() - var responseBodyStr = "" - if (responseStr != null) { - if (responseStr.isNotEmpty()) { - responseBodyStr = responseStr.take(2000) - } - } - log.warn("<-- HTTP Response: code=${response.code}, message=${response.message}, body=${responseBodyStr}") - responseBody?.closeQuietly() - - return chain.proceed(request) - } -} diff --git a/src/main/kotlin/snyk/trust/TrustedProjects.kt b/src/main/kotlin/snyk/trust/TrustedProjects.kt index 508816c03..96cd62c79 100644 --- a/src/main/kotlin/snyk/trust/TrustedProjects.kt +++ b/src/main/kotlin/snyk/trust/TrustedProjects.kt @@ -3,7 +3,7 @@ package snyk.trust import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project @@ -58,7 +58,7 @@ private fun confirmScanningUntrustedProject(project: Project): ScanUntrustedProj var choice = ScanUntrustedProjectChoice.CANCEL - invokeAndWaitIfNeeded { + runInEdt { val result = MessageDialogBuilder .yesNo(title, message) .icon(Messages.getWarningIcon()) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e72125af0..d5e39c49b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,10 @@ com.intellij.modules.xml + + + - - - - - + + + + + diff --git a/src/test/kotlin/io/snyk/plugin/TestUtils.kt b/src/test/kotlin/io/snyk/plugin/TestUtils.kt index 5550032cb..53c1a5575 100644 --- a/src/test/kotlin/io/snyk/plugin/TestUtils.kt +++ b/src/test/kotlin/io/snyk/plugin/TestUtils.kt @@ -43,7 +43,11 @@ fun resetSettings(project: Project?) { SnykProjectSettingsStateService(), project ) - LanguageServerWrapper.getInstance().shutdown() + try { + LanguageServerWrapper.getInstance().shutdown() + } catch (ignore: Exception) { + // ignore + } } /** low level avoiding download the CLI file */ diff --git a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt index 83c5e605b..2918772a7 100644 --- a/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt +++ b/src/test/kotlin/io/snyk/plugin/extensions/SnykControllerImplTest.kt @@ -53,8 +53,8 @@ class SnykControllerImplTest : LightPlatformTestCase() { val controller = SnykControllerImpl(project) controller.scan() - PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - verify { languageServerWrapper.sendScanCommand(project) } + verify (timeout = 5000){ languageServerWrapper.sendScanCommand(project) } } } diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index 935321bfd..c4f8e706e 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -3,7 +3,6 @@ package io.snyk.plugin.services import com.intellij.openapi.components.service import com.intellij.testFramework.LightPlatformTestCase import com.intellij.testFramework.PlatformTestUtil -import com.intellij.testFramework.replaceService import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject @@ -69,7 +68,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { setupDummyCliFile() val snykTaskQueueService = project.service() - snykTaskQueueService.scan(false) + snykTaskQueueService.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) @@ -85,7 +84,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { every { isCliInstalled() } returns true val snykTaskQueueService = project.service() - snykTaskQueueService.scan(false) + snykTaskQueueService.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) @@ -99,7 +98,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { val snykTaskQueueService = project.service() every { isCliInstalled() } returns true - snykTaskQueueService.scan(false) + snykTaskQueueService.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) @@ -156,7 +155,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { every { isCliInstalled() } returns true every { getIacService(project)?.scan() } returns fakeIacResult - snykTaskQueueService.scan(false) + snykTaskQueueService.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertEquals(fakeIacResult, getSnykCachedResults(project)?.currentIacResult) @@ -178,7 +177,7 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { getSnykCachedResults(project)?.currentContainerResult = null - snykTaskQueueService.scan(false) + snykTaskQueueService.scan() PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() await().atMost(2, TimeUnit.SECONDS).until { diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt new file mode 100644 index 000000000..9a7b000ff --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -0,0 +1,116 @@ +package io.snyk.plugin.ui + +import com.intellij.openapi.components.service +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.VirtualFileSystem +import com.intellij.testFramework.LightPlatform4TestCase +import com.intellij.testFramework.PlatformTestUtil +import io.mockk.CapturingSlot +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +import io.snyk.plugin.getContentRootPaths +import io.snyk.plugin.toVirtualFile +import okio.Path.Companion.toPath +import org.eclipse.lsp4j.DidChangeConfigurationParams +import org.eclipse.lsp4j.services.LanguageServer +import org.junit.Test +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.LanguageServerSettings +import snyk.common.lsp.LanguageServerWrapper +import snyk.trust.WorkspaceTrustService +import snyk.trust.WorkspaceTrustSettings +import kotlin.io.path.absolutePathString + + +class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { + private val lsMock: LanguageServer = mockk(relaxed = true) + private lateinit var folderConfig: FolderConfig + lateinit var cut: BranchChooserComboBoxDialog + + override fun setUp(): Unit { + super.setUp() + unmockkAll() + folderConfig = FolderConfig(project.basePath.toString(), "testBranch") + service().addFolderConfig(folderConfig) + project.getContentRootPaths().forEach { service().addTrustedPath(it.root.absolutePathString())} + val languageServerWrapper = LanguageServerWrapper.getInstance() + languageServerWrapper.isInitialized = true + languageServerWrapper.languageServer = lsMock + project.basePath?.let { service().addTrustedPath(it.toPath().parent!!.toNioPath()) } + cut = BranchChooserComboBoxDialog(project) + } + + override fun tearDown() { + super.tearDown() + unmockkAll() + } + + @Test + fun `test execute transmits the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + + cut.comboBoxes = mutableListOf(comboBox) + + cut.execute() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + + + val capturedParam = CapturingSlot() + verify { lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) } + val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + // we expect the selected item + assertEquals("main", transmittedSettings.folderConfigs[0].baseBranch) + } + + @Test + fun `test execute does not transmit the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + cut.comboBoxes = mutableListOf(comboBox) + + cut.execute() + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + + val capturedParam = CapturingSlot() + // we need the config update before the scan + verify(exactly = 1, timeout = 2000) { + lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) + } + + val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + + // we expect the selected item + assertEquals("main", transmittedSettings.folderConfigs[0].baseBranch) + } + + @Test + fun `test doCancelAction does not transmit the folder config to language server`() { + // setup selected item to main + val comboBox = ComboBox(arrayOf("main", "master")).apply { + name = folderConfig.folderPath + selectedItem = "main" + } + + cut.comboBoxes = mutableListOf(comboBox) + + cut.doCancelAction() + + val capturedParam = CapturingSlot() + + // we need the config update before the scan + verify(exactly = 0) { + lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) + } + } +} diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt index 393151542..3ff6990f0 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt @@ -8,8 +8,7 @@ import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import io.mockk.unmockkAll import io.snyk.plugin.resetSettings -import org.junit.Test -import snyk.code.annotator.SnykCodeAnnotator +import snyk.common.annotator.SnykCodeAnnotator import java.nio.file.Paths import java.util.function.BooleanSupplier @@ -39,7 +38,6 @@ class OpenFileLoadHandlerGeneratorTest : BasePlatformTestCase() { generator = OpenFileLoadHandlerGenerator(psiFile.project, virtualFiles) } - @Test fun `test openFile should navigate to source`() { generator.openFile("$fileName:1:2:3:4") val matcher = BooleanSupplier { FileEditorManager.getInstance(project).isFileOpen(psiFile.virtualFile) } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt index 9ff2b1e19..421f22ea0 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt @@ -583,7 +583,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { every { getIacService(project)?.scan() } returns iacResultWithError // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) @@ -690,7 +690,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { setUpContainerTest(containerResultWithError) // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) @@ -717,7 +717,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { setUpContainerTest(fakeContainerResult) // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) // Assertions @@ -751,7 +751,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { mockkObject(SnykBalloonNotificationHelper) // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) // Assertions @@ -783,7 +783,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { setUpContainerTest(fakeContainerResult) // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) // Assertions @@ -876,7 +876,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { setUpContainerTest(containerResult) // actual test run - project.service().scan(false) + project.service().scan() PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) return containerResult diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt index b920408f0..d5530be34 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt @@ -21,7 +21,6 @@ import snyk.UIComponentFinder import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.SnykLanguageClient import java.awt.Container -import java.io.File import java.util.concurrent.CompletableFuture class SnykToolWindowPanelTest : LightPlatform4TestCase() { @@ -102,7 +101,7 @@ class SnykToolWindowPanelTest : LightPlatform4TestCase() { fun `should not display onboarding panel and run scan directly`() { every { settings.token } returns "test-token" every { settings.pluginFirstRun } returns true - justRun { taskQueueService.scan(false) } + justRun { taskQueueService.scan() } cut = SnykToolWindowPanel(project) @@ -111,7 +110,7 @@ class SnykToolWindowPanelTest : LightPlatform4TestCase() { assertNotNull(descriptionPanel) assertEquals(findOnePixelSplitter(vulnerabilityTree), descriptionPanel!!.parent) - verify(exactly = 1) { taskQueueService.scan(false) } + verify(exactly = 1) { taskQueueService.scan() } } //TODO rewrite diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 9e8515a0c..31c6b625f 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -1,27 +1,37 @@ package io.snyk.plugin.ui.toolwindow +import com.intellij.ide.impl.TrustedPathsSettings import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.components.service import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiFile import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.treeStructure.Tree import io.mockk.unmockkAll import io.snyk.plugin.Severity +import io.snyk.plugin.getContentRootPaths import io.snyk.plugin.pluginSettings import io.snyk.plugin.resetSettings +import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import junit.framework.TestCase +import okio.Path.Companion.toPath import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range -import snyk.code.annotator.SnykCodeAnnotator +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.DataFlow +import snyk.common.lsp.FolderConfig +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue +import snyk.trust.WorkspaceTrustService +import snyk.trust.WorkspaceTrustSettings import java.nio.file.Paths import javax.swing.JTree import javax.swing.tree.DefaultMutableTreeNode +import kotlin.io.path.absolutePathString class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { private lateinit var cut: SnykToolWindowSnykScanListenerLS @@ -49,70 +59,81 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { file = myFixture.copyFileToProject(fileName) psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - + val contentRootPaths = project.getContentRootPaths() + service() + .addFolderConfig( + FolderConfig( + contentRootPaths.first().toAbsolutePath().toString(), "main" + ) + ) snykToolWindowPanel = SnykToolWindowPanel(project) rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) + pluginSettings().setDeltaEnabled() + contentRootPaths.forEach { service().addTrustedPath(it.root.absolutePathString())} + } + + override fun tearDown() { + super.tearDown() + unmockkAll() } private fun mockScanIssues( isIgnored: Boolean? = false, hasAIFix: Boolean? = false, ): List { - val issue = - ScanIssue( - id = "id", - title = "title", - severity = Severity.CRITICAL.toString(), - filePath = getTestDataPath(), - range = Range(), - additionalData = - IssueData( - message = "Test message", - leadURL = "", - rule = "", - repoDatasetSize = 1, - exampleCommitFixes = listOf(), - cwe = emptyList(), - text = "", - markers = null, - cols = null, - rows = null, - isSecurityType = true, - priorityScore = 0, - hasAIFix = hasAIFix!!, - dataFlow = listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")), - license = null, - identifiers = null, - description = "", - language = "", - packageManager = "", - packageName = "", - name = "", - version = "", - exploit = null, - CVSSv3 = null, - cvssScore = null, - fixedIn = null, - from = listOf(), - upgradePath = listOf(), - isPatchable = false, - isUpgradable = false, - projectName = "", - displayTargetFile = null, - matchingIssues = listOf(), - lesson = null, - details = "", - ruleId = "", - ), - isIgnored = isIgnored, - ignoreDetails = null, - ) + val issue = ScanIssue( + id = "id", + title = "title", + severity = Severity.CRITICAL.toString(), + filePath = getTestDataPath(), + range = Range(), + additionalData = IssueData( + message = "Test message", + leadURL = "", + rule = "", + repoDatasetSize = 1, + exampleCommitFixes = listOf(), + cwe = emptyList(), + text = "", + markers = null, + cols = null, + rows = null, + isSecurityType = true, + priorityScore = 0, + hasAIFix = hasAIFix!!, + dataFlow = listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")), + license = null, + identifiers = null, + description = "", + language = "", + packageManager = "", + packageName = "", + name = "", + version = "", + exploit = null, + CVSSv3 = null, + cvssScore = null, + fixedIn = null, + from = listOf(), + upgradePath = listOf(), + isPatchable = false, + isUpgradable = false, + projectName = "", + displayTargetFile = null, + matchingIssues = listOf(), + lesson = null, + details = "", + ruleId = "", + ), + isIgnored = isIgnored, + ignoreDetails = null, + ) return listOf(issue) } - fun `testAddInfoTreeNodes does not add new tree nodes for non-code security`() { + fun `testAddInfoTreeNodes adds new tree nodes`() { pluginSettings().isGlobalIgnoresFeatureEnabled = true // setup the rootTreeNode from scratch @@ -120,90 +141,72 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues()) - TestCase.assertEquals(3, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) + TestCase.assertEquals(6, rootTreeNode.childCount) + TestCase.assertEquals(rootTreeNode.children().toList()[0].toString(), " Open Source") + TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") + TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") + TestCase.assertEquals( + "✋ 1 vulnerability found by Snyk, 0 ignored", + rootTreeNode.children().toList()[4].toString(), + ) + TestCase.assertEquals( + rootTreeNode.children().toList()[5].toString(), + "⚡ 1 vulnerabilities can be fixed automatically", + ) } - fun `testAddInfoTreeNodes does not add new tree nodes for code security if ignores are not enabled`() { - pluginSettings().isGlobalIgnoresFeatureEnabled = false + fun `testAddInfoTreeNodes adds new branch selection tree nodes`() { + pluginSettings().isGlobalIgnoresFeatureEnabled = true // setup the rootTreeNode from scratch rootTreeNode = DefaultMutableTreeNode("") rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1, 1) - TestCase.assertEquals(3, rootTreeNode.childCount) - } - - fun `testAddInfoTreeNodes adds new tree nodes for code security if ignores are enabled`() { - pluginSettings().isGlobalIgnoresFeatureEnabled = true - // setup the rootTreeNode from scratch - rootTreeNode = DefaultMutableTreeNode("") - rootTreeNode.add(rootOssIssuesTreeNode) - rootTreeNode.add(rootSecurityIssuesTreeNode) - rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) - TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1, 1) - TestCase.assertEquals(5, rootTreeNode.childCount) + TestCase.assertEquals(6, rootTreeNode.childCount) TestCase.assertEquals(rootTreeNode.children().toList()[0].toString(), " Open Source") TestCase.assertEquals(rootTreeNode.children().toList()[1].toString(), " Code Security") TestCase.assertEquals(rootTreeNode.children().toList()[2].toString(), " Code Quality") + TestCase.assertTrue(rootTreeNode.children().toList()[3].toString().contains("Click to choose base branch for")) TestCase.assertEquals( - rootTreeNode.children().toList()[3].toString(), - "1 vulnerability found by Snyk, 0 ignored", + "✋ 1 vulnerability found by Snyk, 0 ignored", + rootTreeNode.children().toList()[4].toString(), ) TestCase.assertEquals( - rootTreeNode.children().toList()[4].toString(), - "⚡ 1 vulnerabilities can be fixed by Snyk DeepCode AI", + "⚡ 1 vulnerabilities can be fixed automatically", + rootTreeNode.children().toList()[5].toString(), ) } @@ -216,24 +219,22 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(true), 1, 1) - TestCase.assertEquals(6, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(true), 1) + TestCase.assertEquals(7, rootTreeNode.childCount) } fun `testAddInfoTreeNodes adds new tree nodes for code security if all open issues are hidden`() { @@ -245,23 +246,21 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + cut = SnykToolWindowSnykScanListenerLS( + project, + snykToolWindowPanel, + vulnerabilitiesTree, + rootSecurityIssuesTreeNode, + rootQualityIssuesTreeNode, + rootOssIssuesTreeNode, + ) TestCase.assertEquals(3, rootTreeNode.childCount) - cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(false), 1, 1) - TestCase.assertEquals(6, rootTreeNode.childCount) + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(false), 1) + TestCase.assertEquals(7, rootTreeNode.childCount) } } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt index 6cd9d1062..123fbab38 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -17,12 +17,11 @@ import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range -import org.junit.Test import snyk.UIComponentFinder.getJBCEFBrowser import snyk.UIComponentFinder.getJLabelByText import snyk.UIComponentFinder.getJPanelByName -import snyk.code.annotator.SnykCodeAnnotator import snyk.common.ProductType +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.CommitChangeLine import snyk.common.lsp.DataFlow import snyk.common.lsp.ExampleCommitFix @@ -80,7 +79,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { } returns listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")) } - @Test fun `test createUI should build the right panels for Snyk Code if HTML is not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -108,7 +106,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(ossOverviewPanel) } - @Test fun `test createUI should build panel with issue message as overview label if HTML is not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -121,7 +118,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(actualBrowser) } - @Test fun `test createUI should show nothing if HTML is allowed but JCEF is not supported`() { mockkObject(JCEFUtils) every { JCEFUtils.getJBCefBrowserComponentIfSupported(eq("HTML message"), any()) } returns null @@ -137,7 +133,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNull(actualBrowser) } - @Test fun `test createUI should build panel with HTML from details if allowed`() { val mockJBCefBrowserComponent = JLabel("HTML message") mockkObject(JCEFUtils) @@ -156,7 +151,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { assertNotNull(actualBrowser) } - @Test fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "\${ideStyle}HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt index 7738adc1b..633f05eed 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -15,12 +15,11 @@ import io.snyk.plugin.SnykFile import io.snyk.plugin.resetSettings import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.toolwindow.panels.SuggestionDescriptionPanelFromLS -import org.junit.Test import snyk.UIComponentFinder.getActionLinkByText import snyk.UIComponentFinder.getJLabelByText import snyk.UIComponentFinder.getJPanelByName -import snyk.code.annotator.SnykCodeAnnotator import snyk.common.ProductType +import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue import java.nio.file.Paths @@ -79,7 +78,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { } returns emptyList() } - @Test fun `test createUI should build the right panels for Snyk OSS if HTML not allowed`() { every { issue.canLoadSuggestionPanelFromHTML() } returns false @@ -113,7 +111,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { assertNotNull(ossOverviewPanel) } - @Test fun `test createUI should build panel with HTML from details if allowed`() { val mockJBCefBrowserComponent = JLabel("HTML message") mockkObject(JCEFUtils) @@ -132,7 +129,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { assertNotNull(actualBrowser) } - @Test fun `test getStyledHTML should inject CSS into the HTML if allowed`() { every { issue.details() } returns "HTML message" every { issue.canLoadSuggestionPanelFromHTML() } returns true diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/settings/ScanTypesPanelTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/settings/ScanTypesPanelTest.kt index d196cb343..18556e3cd 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/settings/ScanTypesPanelTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/settings/ScanTypesPanelTest.kt @@ -1,6 +1,7 @@ package io.snyk.plugin.ui.toolwindow.settings import com.intellij.openapi.Disposable +import com.intellij.openapi.components.service import com.intellij.openapi.util.Disposer import com.intellij.testFramework.LightPlatform4TestCase import com.intellij.ui.components.JBCheckBox @@ -10,6 +11,7 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify +import io.snyk.plugin.getContentRootPaths import io.snyk.plugin.getKubernetesImageCache import io.snyk.plugin.pluginSettings import io.snyk.plugin.resetSettings @@ -19,6 +21,8 @@ import snyk.common.ProductType import snyk.common.UIComponentFinder.getComponentByName import snyk.common.isSnykCodeAvailable import snyk.container.KubernetesImageCache +import snyk.trust.WorkspaceTrustSettings +import kotlin.io.path.absolutePathString class ScanTypesPanelTest : LightPlatform4TestCase() { private lateinit var disposable: Disposable @@ -26,6 +30,7 @@ class ScanTypesPanelTest : LightPlatform4TestCase() { override fun setUp() { super.setUp() unmockkAll() + project.getContentRootPaths().forEach { service().addTrustedPath(it.root.absolutePathString())} resetSettings(project) disposable = Disposer.newDisposable() } diff --git a/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt b/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt deleted file mode 100644 index 333363e8b..000000000 --- a/src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package snyk.common.intentionactions - -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import io.mockk.every -import io.mockk.mockk -import io.mockk.unmockkAll -import io.snyk.plugin.pluginSettings -import org.junit.Test -import java.nio.file.Paths - -class AlwaysAvailableReplacementIntentionActionTest : BasePlatformTestCase() { - private lateinit var file: VirtualFile - private val replacementText = "b" - private val range = TextRange(/* startOffset = */ 0,/* endOffset = */ 4) - private val familyName = "Snyk" - private lateinit var cut: AlwaysAvailableReplacementIntentionAction - - private val fileName = "package.json" - - private lateinit var psiFile: PsiFile - - override fun getTestDataPath(): String { - val resource = AlwaysAvailableReplacementIntentionAction::class.java - .getResource("/test-fixtures/oss/annotator") - requireNotNull(resource) { "Make sure that the resource $resource exists!" } - return Paths.get(resource.toURI()).toString() - } - - override fun isWriteActionRequired(): Boolean = true - - override fun setUp() { - super.setUp() - unmockkAll() - pluginSettings().fileListenerEnabled = false - file = myFixture.copyFileToProject(fileName) - psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - cut = AlwaysAvailableReplacementIntentionAction( - range = range, - replacementText = replacementText - ) - } - - override fun tearDown() { - unmockkAll() - pluginSettings().fileListenerEnabled = true - super.tearDown() - } - - @Test - fun `test getText`() { - assertTrue(cut.text.contains(replacementText)) - } - - @Test - fun `test familyName`() { - assertEquals(familyName, cut.familyName) - } - - @Test - fun `test invoke`() { - val editor = mockk() - val doc = psiFile.viewProvider.document - doc!!.setText("abcd") - every { editor.document } returns doc - - cut.invoke(project, editor, psiFile) - - assertEquals("b", doc.text) - } -} diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 4b92d2594..338ae9734 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -34,6 +34,7 @@ import snyk.trust.WorkspaceTrustService import java.util.concurrent.CompletableFuture class LanguageServerWrapperTest { + private val folderConfigSettingsMock: FolderConfigSettings = mockk(relaxed = true) private val applicationMock: Application = mockk() private val projectMock: Project = mockk() private val lsMock: LanguageServer = mockk() @@ -56,6 +57,7 @@ class LanguageServerWrapperTest { val projectManagerMock = mockk() every { applicationMock.getService(ProjectManager::class.java) } returns projectManagerMock every { applicationMock.getService(SnykPluginDisposable::class.java) } returns snykPluginDisposable + every { applicationMock.getService(FolderConfigSettings::class.java) } returns folderConfigSettingsMock every { applicationMock.isDisposed } returns false every { projectManagerMock.openProjects } returns arrayOf(projectMock) @@ -95,6 +97,7 @@ class LanguageServerWrapperTest { verify { lsMock.initialize(any()) } verify { lsMock.initialized(any()) } + verify { folderConfigSettingsMock.getAll() } } @Test diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 183f1d1a8..cd5d769f5 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -1,6 +1,10 @@ package snyk.container import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.vfs.VirtualFile @@ -14,7 +18,6 @@ import com.intellij.testFramework.PlatformTestUtil import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.io.createDirectories import com.intellij.util.io.delete -import io.mockk.justRun import io.mockk.mockk import io.mockk.unmockkAll import io.snyk.plugin.getKubernetesImageCache @@ -29,6 +32,7 @@ import java.io.File import java.nio.file.Files import java.nio.file.LinkOption import java.nio.file.Paths +import java.time.Duration import java.util.concurrent.TimeUnit import kotlin.io.path.notExists @@ -71,12 +75,17 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { setUpContainerTest() val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) + var virtualFile: VirtualFile? = null VirtualFileManager.getInstance().syncRefresh() - val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) - require(virtualFile != null) + runInEdt { + virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) + } + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + await().timeout(5, TimeUnit.SECONDS).until { virtualFile?.isValid ?: false } + ApplicationManager.getApplication().runWriteAction { - val file = PsiManager.getInstance(project).findFile(virtualFile) + val file = PsiManager.getInstance(project).findFile(virtualFile!!) require(file != null) PsiDocumentManager.getInstance(project).getDocument(file) ?.setText(TestYamls.podYaml()) @@ -93,7 +102,7 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { assertEquals(1, kubernetesWorkloadImages.size) assertEquals(path, kubernetesWorkloadImages.first().virtualFile.toNioPath()) assertEquals("nginx:1.16.0", kubernetesWorkloadImages.first().image) - virtualFile.toNioPath().delete(true) + virtualFile!!.toNioPath().delete(true) } fun `test Container should delete images from cache when yaml file is deleted`() { diff --git a/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt b/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt index f0d70d0b3..2756f1941 100644 --- a/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt +++ b/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt @@ -1,12 +1,14 @@ package snyk.container import com.google.gson.Gson +import com.intellij.openapi.components.service import com.intellij.testFramework.LightPlatform4TestCase import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.unmockkAll import io.mockk.verify +import io.snyk.plugin.getContentRootPaths import io.snyk.plugin.removeDummyCliFile import io.snyk.plugin.setupDummyCliFile import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel @@ -16,9 +18,10 @@ import org.junit.Test import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.commands.COMMAND_EXECUTE_CLI import snyk.container.TestYamls.podYaml +import snyk.trust.WorkspaceTrustSettings import java.util.concurrent.CompletableFuture +import kotlin.io.path.absolutePathString -@Suppress("FunctionName") class ContainerServiceIntegTest : LightPlatform4TestCase() { private lateinit var cut: ContainerService private val containerResultWithRemediationJson = javaClass.classLoader @@ -51,6 +54,7 @@ class ContainerServiceIntegTest : LightPlatform4TestCase() { super.setUp() unmockkAll() setupDummyCliFile() + project.getContentRootPaths().forEach { service().addTrustedPath(it.root.absolutePathString())} cut = ContainerService(project) val languageServerWrapper = LanguageServerWrapper.getInstance() languageServerWrapper.languageServer = lsMock diff --git a/src/test/kotlin/snyk/net/HttpClientTest.kt b/src/test/kotlin/snyk/net/HttpClientTest.kt deleted file mode 100644 index 9a084edf3..000000000 --- a/src/test/kotlin/snyk/net/HttpClientTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package snyk.net - -import io.mockk.every -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.unmockkAll -import io.mockk.verify -import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import junit.framework.TestCase.assertFalse -import org.junit.After -import org.junit.Before -import org.junit.Test -import javax.net.ssl.TrustManagerFactory - -class HttpClientTest { - - @Before - fun setUp() { - unmockkAll() - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `should use SSLContext with TLSv12 configured`() { - val client = HttpClient().build() - assertFalse(client.sslSocketFactory.supportedCipherSuites.contains("TLS_RSA_WITH_DES_CBC_SHA")) - } - - @Test(expected = IllegalStateException::class) - fun `should display balloon error message if ssl context cannot be initialized`() { - mockkStatic(TrustManagerFactory::class) - val trustManagerFactory = mockk(relaxed = true) - every { TrustManagerFactory.getInstance(any()) } returns trustManagerFactory - val exception = IllegalStateException("Test exception. Don't panic") - every { trustManagerFactory.trustManagers } throws exception - - mockkObject(SnykBalloonNotificationHelper) - justRun { SnykBalloonNotificationHelper.showError(any(), null, any()) } - - try { - HttpClient().build() - } finally { - val balloonMessage = - String.format(HttpClient.BALLOON_MESSAGE_ILLEGAL_STATE_EXCEPTION, exception.localizedMessage) - verify(exactly = 1) { SnykBalloonNotificationHelper.showError(balloonMessage, null, any()) } - } - } -}
Select the Snyk vulnerability scanning plugin