From d6d3a41b633832045cc5bdbf716d4187a288db19 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 09:44:32 +0200 Subject: [PATCH 01/26] feat: add gutter icons for found issues (#594) * fix: mark our annotators as dumbaware * docs: update CHANGELOG.md * feat: add gutter icons for snyk findings * feat: add gutter icons for snyk findings * feat: sort annotations before applying --- CHANGELOG.md | 2 + .../annotator/ShowDetailsIntentionAction.kt | 2 +- .../snyk/code/annotator/SnykAnnotator.kt | 96 +++++++++++++++---- ...waysAvailableReplacementIntentionAction.kt | 45 --------- ...AvailableReplacementIntentionActionTest.kt | 75 --------------- 5 files changed, 79 insertions(+), 141 deletions(-) delete mode 100644 src/main/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionAction.kt delete mode 100644 src/test/kotlin/snyk/common/intentionactions/AlwaysAvailableReplacementIntentionActionTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 974f6b2f0..5633ad345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - guard base branch setting against being empty - better error messaging when unexpected loop occurs during initialization - switch downloads to downloads.snyk.io +- allow annotations during IntelliJ indexing +- add gutter icons for Snyk issues ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt index 3f88f90df..97ba5cf61 100644 --- a/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt +++ b/src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt @@ -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/code/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt index 322bb70ed..673b05ef8 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt @@ -1,15 +1,24 @@ package snyk.code.annotator +import com.intellij.codeInsight.inline.completion.InlineCompletionEventType 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.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.diagnostic.logger +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.psi.PsiFile +import icons.SnykIcons import io.snyk.plugin.getSnykCachedResultsForProduct +import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams @@ -22,11 +31,12 @@ import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException +import javax.swing.Icon private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : - ExternalAnnotator>, List>(), Disposable { + ExternalAnnotator>, List>(), Disposable, DumbAware { val logger = logger() protected var disposed = false get() { @@ -43,7 +53,8 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, - val intention: IntentionAction + val intention: IntentionAction, + val renderGutterIcon: Boolean = false ) // overrides needed for the Annotator to invoke apply(). We don't do anything here @@ -60,7 +71,8 @@ abstract class SnykAnnotator(private val product: ProductType) : if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() val annotations = mutableListOf() - initial.second.forEach { issue -> + val gutterIcons : MutableSet = mutableSetOf() + initial.second.sortedBy { it.getSeverityAsEnum().getHighlightSeverity() }.forEach { issue -> val textRange = textRange(initial.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() @@ -69,14 +81,16 @@ abstract class SnykAnnotator(private val product: ProductType) : return@forEach } if (!textRange.isEmpty) { - annotations.add( - SnykAnnotation( - highlightSeverity, - annotationMessage, - textRange, - ShowDetailsIntentionAction(annotationMessage, issue) - ) + val detailAnnotation = SnykAnnotation( + highlightSeverity, + annotationMessage, + textRange, + ShowDetailsIntentionAction(annotationMessage, issue), + renderGutterIcon = !gutterIcons.contains(textRange) ) + annotations.add(detailAnnotation) + gutterIcons.add(textRange) + val params = CodeActionParams( TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()), @@ -102,14 +116,13 @@ abstract class SnykAnnotator(private val product: ProductType) : .sortedBy { it.right.title }.forEach { action -> val codeAction = action.right val title = codeAction.title - annotations.add( - SnykAnnotation( - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) + val codeActionAnnotation = SnykAnnotation( + highlightSeverity, + title, + textRange, + CodeActionIntention(issue, codeAction, product) ) + annotations.add(codeActionAnnotation) } } @@ -124,12 +137,15 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.forEach { annotation -> + annotationResult.sortedBy { it.annotationSeverity }.forEach { annotation -> if (!annotation.range.isEmpty) { - holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .withFix(annotation.intention) - .create() + if (annotation.renderGutterIcon) { + annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + } + annoBuilder.create() } } } @@ -179,3 +195,43 @@ abstract class SnykAnnotator(private val product: ProductType) : } } } + +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.TOOL_WINDOW + } + + override fun getClickAction(): AnAction? { + if (annotation.intention !is ShowDetailsIntentionAction) return null + return object: AnAction() { + override fun actionPerformed(e: AnActionEvent) { + invokeLater { + val virtualFile = annotation.intention.issue.virtualFile ?: return@invokeLater + val project = guessProjectForFile(virtualFile) ?: return@invokeLater + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater + + annotation.intention.selectNodeAndDisplayDescription(toolWindowPanel) + } + + } + } + } + + override fun isNavigateAction(): Boolean { + return true + } + + override fun isDumbAware(): Boolean { + return true + } + + +} 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/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) - } -} From dd08eefb087d70db393347b9a0b8b8f7b2dd6311 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 16:25:22 +0200 Subject: [PATCH 02/26] feat: transmit project specific additional params [IDE-567] (#592) * feat: transmit project specific additional params * chore: remove analytics & error reporting settings * fix: ensure sorting descended * feat: switch gutter icons to severity icons, have tooltip * feat: add color settings for annotations * chore: fix code smells * fix: tests * fix: default to theme annotation defaults * chore: remove todo --- CHANGELOG.md | 1 + .../SnykProjectSettingsConfigurable.kt | 22 +++--- .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 30 +------- .../annotator/CodeActionIntention.kt | 9 ++- .../common/annotator/ColorSettingsPage.kt | 55 +++++++++++++++ .../annotator/ShowDetailsIntentionAction.kt | 2 +- .../annotator/SnykAnnotator.kt | 68 ++++++++++++++----- .../annotator/SnykCodeAnnotator.kt | 2 +- .../annotator/SnykOSSAnnotator.kt | 2 +- .../snyk/common/lsp/FolderConfigSettings.kt | 2 +- .../snyk/common/lsp/LanguageServerSettings.kt | 4 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 2 +- src/main/kotlin/snyk/common/lsp/Types.kt | 3 +- src/main/resources/META-INF/plugin.xml | 5 +- .../jcef/OpenFileLoadHandlerGeneratorTest.kt | 4 +- .../SnykToolWindowSnykScanListenerLSTest.kt | 2 +- ...uggestionDescriptionPanelFromLSCodeTest.kt | 8 +-- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 6 +- .../common/lsp/LanguageServerWrapperTest.kt | 3 + 19 files changed, 142 insertions(+), 88 deletions(-) rename src/main/kotlin/snyk/{code => common}/annotator/CodeActionIntention.kt (95%) create mode 100644 src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt rename src/main/kotlin/snyk/{code => common}/annotator/ShowDetailsIntentionAction.kt (94%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykAnnotator.kt (77%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykCodeAnnotator.kt (95%) rename src/main/kotlin/snyk/{code => common}/annotator/SnykOSSAnnotator.kt (96%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633ad345..756b0b069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues +- add color and highlighting setting for Snyk issues ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index c9e9b09f7..b461d3bc4 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.getContentRootPaths 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 @@ -39,8 +42,6 @@ class SnykProjectSettingsConfigurable( override fun isModified(): Boolean = isCoreParamsModified() || isIgnoreUnknownCAModified() || - isSendUsageAnalyticsModified() || - isCrashReportingModified() || snykSettingsDialog.isScanTypeChanged() || snykSettingsDialog.isSeverityEnablementChanged() || snykSettingsDialog.isIssueOptionChanged() || @@ -89,9 +90,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 +103,14 @@ class SnykProjectSettingsConfigurable( if (isProjectSettingsAvailable(project)) { val snykProjectSettingsService = getSnykProjectSettingsService(project) snykProjectSettingsService?.additionalParameters = snykSettingsDialog.getAdditionalParameters() + val fcs = service() + project.getContentRootPaths().forEach { + val fc = fcs.getFolderConfig(it.toAbsolutePath().toString()) + if (fc != null) { + val newFC = fc.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) + fcs.addFolderConfig(newFC) + } + } } runBackgroundableTask("processing config changes", project, true) { @@ -169,12 +175,6 @@ 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 diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index b63979de8..d444007f3 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -99,9 +99,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 = @@ -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 @@ -556,26 +552,6 @@ class SnykSettingsDialog( ), ) - usageAnalyticsCheckBox.text = "Send usage statistics to Snyk" - userExperiencePanel.add( - usageAnalyticsCheckBox, - baseGridConstraints( - row = 1, - anchor = UIGridConstraints.ANCHOR_NORTHWEST, - indent = 0, - ), - ) - - crashReportingCheckBox.text = "Send error reports to Snyk" - userExperiencePanel.add( - crashReportingCheckBox, - baseGridConstraints( - row = 2, - anchor = UIGridConstraints.ANCHOR_NORTHWEST, - indent = 0, - ), - ) - /** Spacer ------------------ */ val generalSettingsSpacer = Spacer() @@ -700,12 +676,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() 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..0abc985af --- /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 Issue\n" + + "This is a demo of a Snyk High Issue\n" + + "This is a demo of a Snyk Medium Issue\n" + + "This is a demo of a Snyk Low Issue\n" + + "This is a demo of a Unknown High 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 94% rename from src/main/kotlin/snyk/code/annotator/ShowDetailsIntentionAction.kt rename to src/main/kotlin/snyk/common/annotator/ShowDetailsIntentionAction.kt index 97ba5cf61..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 diff --git a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt similarity index 77% rename from src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt rename to src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index 673b05ef8..9a141e761 100644 --- a/src/main/kotlin/snyk/code/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -1,6 +1,5 @@ -package snyk.code.annotator +package snyk.common.annotator -import com.intellij.codeInsight.inline.completion.InlineCompletionEventType import com.intellij.codeInsight.intention.IntentionAction import com.intellij.lang.annotation.AnnotationHolder import com.intellij.lang.annotation.ExternalAnnotator @@ -11,12 +10,14 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.invokeLater 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.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 @@ -24,9 +25,14 @@ 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.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.ScanIssue import java.util.concurrent.TimeUnit @@ -37,6 +43,7 @@ private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : ExternalAnnotator>, List>(), Disposable, DumbAware { + val logger = logger() protected var disposed = false get() { @@ -50,6 +57,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } inner class SnykAnnotation( + val issue: ScanIssue, val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, @@ -71,8 +79,8 @@ abstract class SnykAnnotator(private val product: ProductType) : if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() val annotations = mutableListOf() - val gutterIcons : MutableSet = mutableSetOf() - initial.second.sortedBy { it.getSeverityAsEnum().getHighlightSeverity() }.forEach { issue -> + val gutterIcons: MutableSet = mutableSetOf() + initial.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> val textRange = textRange(initial.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() @@ -82,6 +90,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } if (!textRange.isEmpty) { val detailAnnotation = SnykAnnotation( + issue, highlightSeverity, annotationMessage, textRange, @@ -117,6 +126,7 @@ abstract class SnykAnnotator(private val product: ProductType) : val codeAction = action.right val title = codeAction.title val codeActionAnnotation = SnykAnnotation( + issue, highlightSeverity, title, textRange, @@ -137,16 +147,28 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.sortedBy { it.annotationSeverity }.forEach { annotation -> - if (!annotation.range.isEmpty) { - val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) - .range(annotation.range) - .withFix(annotation.intention) - if (annotation.renderGutterIcon) { - annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + annotationResult.sortedByDescending { it.issue.getSeverityAsEnum() } + .forEach { annotation -> + if (!annotation.range.isEmpty) { + val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + .range(annotation.range) + .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) + .withFix(annotation.intention) + if (annotation.renderGutterIcon) { + annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) + } + annoBuilder.create() } - annoBuilder.create() } + } + + private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { + return when(severity) { + Severity.UNKNOWN -> unknown + Severity.LOW -> low + Severity.MEDIUM -> medium + Severity.HIGH -> high + Severity.CRITICAL -> critical } } @@ -206,23 +228,33 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon } override fun getIcon(): Icon { - return SnykIcons.TOOL_WINDOW + return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) } override fun getClickAction(): AnAction? { if (annotation.intention !is ShowDetailsIntentionAction) return null - return object: AnAction() { + return getShowDetailsNavigationAction(annotation.intention) + } + + private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = + object : AnAction() { override fun actionPerformed(e: AnActionEvent) { invokeLater { - val virtualFile = annotation.intention.issue.virtualFile ?: return@invokeLater + val virtualFile = intention.issue.virtualFile ?: return@invokeLater val project = guessProjectForFile(virtualFile) ?: return@invokeLater val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater - annotation.intention.selectNodeAndDisplayDescription(toolWindowPanel) + intention.selectNodeAndDisplayDescription(toolWindowPanel) } - } } + + override fun getTooltipText(): String { + return annotation.annotationMessage + } + + override fun getAccessibleName(): String { + return annotation.annotationMessage } override fun isNavigateAction(): Boolean { 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/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/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt index fc6621b26..8fa59dfee 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt @@ -21,7 +21,7 @@ class FolderConfigSettings : SimplePersistentStateComponent() } - private fun addFolderConfig(folderConfig: FolderConfig) { + fun addFolderConfig(folderConfig: FolderConfig) { state.configs[folderConfig.folderPath] = gson.toJson(folderConfig) } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index 4445a112c..f48549abc 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,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.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 9dbe962e7..339b344b3 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -140,7 +140,7 @@ class LanguageServerWrapper( if (!disposed) { try { process.errorStream.bufferedReader().forEachLine { println(it) } - } catch (ignored: RuntimeException) { + } catch (ignored: Exception) { // ignore } } diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 5d1b99310..a30700a1d 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -623,5 +623,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/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e72125af0..e1cbb3b6d 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,7 @@ com.intellij.modules.xml + - - + + 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/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 9e8515a0c..d2216379d 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -15,7 +15,7 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode import junit.framework.TestCase 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.IssueData import snyk.common.lsp.ScanIssue 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 a717a650b..7ad4b5661 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 418009594..e80feed1c 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/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 From 337af1a7df8d06db8977f000ae7d460c69aa8b22 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 4 Sep 2024 16:43:51 +0200 Subject: [PATCH 03/26] chore: rephrase color setting examples (#597) --- .../kotlin/snyk/common/annotator/ColorSettingsPage.kt | 10 +++++----- src/main/kotlin/snyk/common/lsp/Types.kt | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt index 0abc985af..bd44321b5 100644 --- a/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt +++ b/src/main/kotlin/snyk/common/annotator/ColorSettingsPage.kt @@ -32,11 +32,11 @@ class SnykAnnotationColorSettingsPage : ColorSettingsPage { override fun getHighlighter(): SyntaxHighlighter = PlainSyntaxHighlighter() override fun getDemoText(): String = - "This is a demo of a Snyk Critical Issue\n" + - "This is a demo of a Snyk High Issue\n" + - "This is a demo of a Snyk Medium Issue\n" + - "This is a demo of a Snyk Low Issue\n" + - "This is a demo of a Unknown High Issue\n" + "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( diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index a30700a1d..5f60e5204 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -286,11 +286,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() } } From 10962dd82b30a8ab622c9d15339bf64e3e18958e Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Thu, 5 Sep 2024 16:12:57 +0200 Subject: [PATCH 04/26] feat: add dialog to choose reference branch [IDE-601] (#600) * fix: UI thread usage during node searching /selection * chore: enable info nodes for OSS and Code always * fix: save settings when token is received * feat: add dialog to choose a reference branch * docs: update CHANGELOG.md * fix: tests * chore: add test for branch choosing tree node * chore: optimize import * fix: tests * feat: add test for branch chooser --- CHANGELOG.md | 2 + .../SnykApplicationSettingsStateService.kt | 1 + .../SnykProjectSettingsConfigurable.kt | 11 +- .../plugin/ui/BranchChooserComboboxDialog.kt | 83 ++++++ .../ui/toolwindow/SnykToolWindowPanel.kt | 27 +- .../SnykToolWindowSnykScanListenerLS.kt | 78 ++--- .../ui/toolwindow/SnykTreeCellRenderer.kt | 6 + .../nodes/secondlevel/InfoTreeNode.kt | 11 +- .../snyk/common/annotator/SnykAnnotator.kt | 12 +- .../snyk/common/lsp/FolderConfigSettings.kt | 28 +- .../snyk/common/lsp/SnykLanguageClient.kt | 2 + .../ui/BranchChooserComboBoxDialogTest.kt | 109 +++++++ .../SnykToolWindowSnykScanListenerLSTest.kt | 277 +++++++++--------- .../ContainerBulkFileListenerTest.kt | 12 +- 14 files changed, 447 insertions(+), 212 deletions(-) create mode 100644 src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt create mode 100644 src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 756b0b069..d350c3fa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues - add color and highlighting setting for Snyk issues +- add dialog to choose reference branch when delta scanning +- always display info nodes ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index fa3a2ba69..a813b380f 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -37,6 +37,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent() - project.getContentRootPaths().forEach { - val fc = fcs.getFolderConfig(it.toAbsolutePath().toString()) - if (fc != null) { - val newFC = fc.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) - fcs.addFolderConfig(newFC) - } - } + fcs.getAllForProject(project) + .map { it.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) } + .forEach { fcs.addFolderConfig(it) } } runBackgroundableTask("processing config changes", project, true) { 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/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index c82ac0c9e..41beb5c27 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) { @@ -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 @@ -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..450485a99 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,11 +74,11 @@ 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 } } @@ -83,11 +87,11 @@ class SnykToolWindowSnykScanListenerLS( 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 } } @@ -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,39 @@ 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 + // TODO: check for delta findings + val deltaFindingsEnabled = true + if (deltaFindingsEnabled) { + // 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" + text = if (issuesCount == 1) { + "$issuesCount vulnerability found by Snyk" } else { - text = "✋ $issuesCount vulnerabilities found by Snyk" + "✋ $issuesCount vulnerabilities found by Snyk" + } + if (pluginSettings().isGlobalIgnoresFeatureEnabled) { + text += ", $ignoredIssuesCount ignored" } - text += ", $ignoredIssuesCount ignored" } rootNode.add( InfoTreeNode( @@ -297,31 +308,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/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index 9a141e761..f621f1967 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -25,6 +25,7 @@ import org.eclipse.lsp4j.CodeActionContext import org.eclipse.lsp4j.CodeActionParams import org.eclipse.lsp4j.Range import org.eclipse.lsp4j.TextDocumentIdentifier +import org.jetbrains.concurrency.runAsync import snyk.common.AnnotatorCommon import snyk.common.ProductType import snyk.common.annotator.SnykAnnotationAttributeKey.critical @@ -163,7 +164,7 @@ abstract class SnykAnnotator(private val product: ProductType) : } private fun getTextAttributeKeyBySeverity(severity: Severity): TextAttributesKey { - return when(severity) { + return when (severity) { Severity.UNKNOWN -> unknown Severity.LOW -> low Severity.MEDIUM -> medium @@ -239,11 +240,10 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon private fun getShowDetailsNavigationAction(intention: ShowDetailsIntentionAction) = object : AnAction() { override fun actionPerformed(e: AnActionEvent) { - invokeLater { - val virtualFile = intention.issue.virtualFile ?: return@invokeLater - val project = guessProjectForFile(virtualFile) ?: return@invokeLater - val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@invokeLater - + runAsync { + val virtualFile = intention.issue.virtualFile ?: return@runAsync + val project = guessProjectForFile(virtualFile) ?: return@runAsync + val toolWindowPanel = getSnykToolWindowPanel(project) ?: return@runAsync intention.selectNodeAndDisplayDescription(toolWindowPanel) } } diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt index 8fa59dfee..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( @@ -25,11 +28,23 @@ class FolderConfigSettings : SimplePersistentStateComponent { + 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/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 03e609dc7..29f192460 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -19,6 +19,7 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VfsUtilCore @@ -304,6 +305,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() 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..17c14e897 --- /dev/null +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -0,0 +1,109 @@ +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.testFramework.LightPlatform4TestCase +import com.intellij.testFramework.PlatformTestUtil +import io.mockk.CapturingSlot +import io.mockk.mockk +import io.mockk.unmockkAll +import io.mockk.verify +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 + + +class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { + private val lsMock: LanguageServer = mockk(relaxed = true) + 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.basePath?.let { service().addTrustedPath(it.toNioPathOrNull()!!) } + val languageServerWrapper = LanguageServerWrapper.getInstance() + languageServerWrapper.isInitialized = true + languageServerWrapper.languageServer = lsMock + 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/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index d2216379d..55c72350c 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -1,12 +1,14 @@ package io.snyk.plugin.ui.toolwindow 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.ui.toolwindow.nodes.root.RootOssTreeNode @@ -17,6 +19,8 @@ import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range 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 java.nio.file.Paths @@ -49,7 +53,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { file = myFixture.copyFileToProject(fileName) psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - + service() + .addFolderConfig( + FolderConfig( + project.getContentRootPaths().first().toAbsolutePath().toString(), "main" + ) + ) snykToolWindowPanel = SnykToolWindowPanel(project) rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) @@ -60,59 +69,57 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { 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 +127,72 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + 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( + rootTreeNode.children().toList()[4].toString(), + "1 vulnerability found by Snyk, 0 ignored", + ) + 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 - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + 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 + cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) - // 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, - ) - - 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(), + rootTreeNode.children().toList()[4].toString(), "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( - rootTreeNode.children().toList()[4].toString(), - "⚡ 1 vulnerabilities can be fixed by Snyk DeepCode AI", + rootTreeNode.children().toList()[5].toString(), + "⚡ 1 vulnerabilities can be fixed automatically", ) } @@ -216,24 +205,22 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + 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 +232,21 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) - vulnerabilitiesTree = - Tree(rootTreeNode).apply { - this.isRootVisible = false - } - - cut = - SnykToolWindowSnykScanListenerLS( - project, - snykToolWindowPanel, - vulnerabilitiesTree, - rootSecurityIssuesTreeNode, - rootQualityIssuesTreeNode, - rootOssIssuesTreeNode, - ) + vulnerabilitiesTree = Tree(rootTreeNode).apply { + this.isRootVisible = false + } + + 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/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 183f1d1a8..6c05e2797 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -14,7 +14,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 @@ -72,11 +71,14 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) VirtualFileManager.getInstance().syncRefresh() - val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) - require(virtualFile != null) + var virtualFile: VirtualFile? = null + await().atMost(2, TimeUnit.SECONDS).until { + virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) + 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 +95,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`() { From 6d669d1d5f7ee28c878b1f9ba83c2dc73d94f6d7 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 6 Sep 2024 09:21:33 +0200 Subject: [PATCH 05/26] feat: add option to enable/disable gutter icons (#601) * feat: add option to enable/disable gutter icons * chore: small performance fix --- CHANGELOG.md | 1 + .../snyk/common/annotator/SnykAnnotator.kt | 135 +++++++++++------- .../annotator/SnykLineMarkerProvider.kt | 23 +++ src/main/resources/META-INF/plugin.xml | 3 + 4 files changed, 114 insertions(+), 48 deletions(-) create mode 100644 src/main/kotlin/snyk/common/annotator/SnykLineMarkerProvider.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index d350c3fa1..cca560442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues +- add option to switch gutter icons on/off in Snyk Settings (the IntelliJ setting only works for parsed languages) - add color and highlighting setting for Snyk issues - add dialog to choose reference branch when delta scanning - always display info nodes diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index f621f1967..fa7a94879 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -1,6 +1,9 @@ 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 @@ -8,7 +11,6 @@ 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.application.invokeLater import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.colors.TextAttributesKey import com.intellij.openapi.editor.markup.GutterIconRenderer @@ -40,6 +42,7 @@ import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.Icon + private const val CODEACTION_TIMEOUT = 5000L abstract class SnykAnnotator(private val product: ProductType) : @@ -63,26 +66,29 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationMessage: String, val range: TextRange, val intention: IntentionAction, - val renderGutterIcon: Boolean = false + var gutterIconRenderer: GutterIconRenderer? = null ) // 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 }) + .distinctBy { it.id }) } - override fun doAnnotate(initial: Pair>): List { + override fun doAnnotate(initialInfo: Pair>): List { if (disposed) return emptyList() - AnnotatorCommon.prepareAnnotate(initial.first) + AnnotatorCommon.prepareAnnotate(initialInfo.first) if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() + val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() + val gutterIconEnabled = LineMarkerSettings.getSettings().isEnabled(lineMarkerProviderDescriptor) + val annotations = mutableListOf() val gutterIcons: MutableSet = mutableSetOf() - initial.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> - val textRange = textRange(initial.first, issue.range) + + initialInfo.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> + val textRange = textRange(initialInfo.first, issue.range) val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity() val annotationMessage = issue.annotationMessage() if (textRange == null) { @@ -95,47 +101,28 @@ abstract class SnykAnnotator(private val product: ProductType) : highlightSeverity, annotationMessage, textRange, - ShowDetailsIntentionAction(annotationMessage, issue), - renderGutterIcon = !gutterIcons.contains(textRange) + ShowDetailsIntentionAction(annotationMessage, issue) ) - annotations.add(detailAnnotation) - gutterIcons.add(textRange) - 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() + val gutterIconRenderer = + if (gutterIconEnabled && !gutterIcons.contains(textRange)) { + gutterIcons.add(textRange) + SnykShowDetailsGutterRenderer(detailAnnotation) + } else { + null } - 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 - val codeActionAnnotation = SnykAnnotation( - issue, - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) - annotations.add(codeActionAnnotation) - } + detailAnnotation.gutterIconRenderer = gutterIconRenderer + annotations.add(detailAnnotation) + annotations.addAll( + getAnnotationsForCodeActions( + initialInfo, + issue, + highlightSeverity, + textRange + ) + ) } } return annotations @@ -148,14 +135,14 @@ abstract class SnykAnnotator(private val product: ProductType) : ) { if (disposed) return if (!LanguageServerWrapper.getInstance().isInitialized) return - annotationResult.sortedByDescending { it.issue.getSeverityAsEnum() } + annotationResult .forEach { annotation -> if (!annotation.range.isEmpty) { val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) .withFix(annotation.intention) - if (annotation.renderGutterIcon) { + if (annotation.gutterIconRenderer != null) { annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) } annoBuilder.create() @@ -163,6 +150,60 @@ abstract class SnykAnnotator(private val product: ProductType) : } } + private fun getAnnotationsForCodeActions( + initial: Pair>, + issue: ScanIssue, + highlightSeverity: HighlightSeverity, + textRange: TextRange, + ): MutableList { + val addedAnnotationsList = mutableListOf() + 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 + val codeActionAnnotation = SnykAnnotation( + issue, + highlightSeverity, + title, + textRange, + CodeActionIntention(issue, codeAction, product) + ) + addedAnnotationsList.add(codeActionAnnotation) + } + return addedAnnotationsList + } + + 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 @@ -264,6 +305,4 @@ class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIcon override fun isDumbAware(): Boolean { return true } - - } 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/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e1cbb3b6d..d2fc3bd25 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,9 @@ com.intellij.modules.xml + Date: Fri, 6 Sep 2024 09:39:15 +0200 Subject: [PATCH 06/26] fix: changelog (#602) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca560442..ebc157020 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - switch downloads to downloads.snyk.io - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues -- add option to switch gutter icons on/off in Snyk Settings (the IntelliJ setting only works for parsed languages) +- add option to switch gutter icons on/off - add color and highlighting setting for Snyk issues - add dialog to choose reference branch when delta scanning - always display info nodes From a530ebb8569f8e574b3de8b5b570e9a03ad04d1d Mon Sep 17 00:00:00 2001 From: Knut Funkel Date: Fri, 6 Sep 2024 10:25:04 +0200 Subject: [PATCH 07/26] feat: add net new issues setting to settings page [IDE-598] (#589) * feat: add net new issues setting to settings page * fix: set the language server setting for enableDeltaFindings * wip: adding setting for delta scans * chore: update changelog * fix: update the Net new settings text * chore: refactor delta finding settings * fix: refactor isNetNewIssuesSelected -> getNetNewIssuesSelected --- CHANGELOG.md | 2 +- .../SnykApplicationSettingsStateService.kt | 4 ++ .../SnykProjectSettingsConfigurable.kt | 51 +++++++------ .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 71 +++++++++++++++++-- .../snyk/common/lsp/LanguageServerSettings.kt | 1 + .../snyk/common/lsp/LanguageServerWrapper.kt | 2 + 6 files changed, 102 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc157020..1ca913e35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,6 @@ # Snyk Security Changelog ## [2.9.1] - ### Changed - save git folder config in settings - propagate Jetbrains determined runtime environment to language server @@ -9,6 +8,7 @@ - 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 in settings - allow annotations during IntelliJ indexing - add gutter icons for Snyk issues - add option to switch gutter icons on/off diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index a813b380f..3ae24a2e8 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -35,6 +35,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().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 } } @@ -408,7 +411,8 @@ 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, @@ -459,6 +463,60 @@ class SnykSettingsDialog( ), ) + val netNewIssuesPanel = JPanel(UIGridLayoutManager(2, 4, JBUI.emptyInsets(), -1, -1)) + + productAndSeveritiesPanel.add( + netNewIssuesPanel, + baseGridConstraints( + row = 1, + column = 0, + anchor = UIGridConstraints.ANCHOR_NORTHWEST, + fill = UIGridConstraints.FILL_NONE, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, + indent = 0, + ), + ) + + val newNewIssuesLabel = JLabel("All Issues Vs Net New Issues:") + newNewIssuesLabel.labelFor = netNewIssuesDropDown + netNewIssuesPanel.add( + newNewIssuesLabel, + baseGridConstraintsAnchorWest( + row = 0, + indent = 0, + ), + ) + + netNewIssuesPanel.add( + netNewIssuesDropDown, + baseGridConstraints( + row = 0, + column = 1, + anchor = UIGridConstraints.ANCHOR_WEST, + fill = UIGridConstraints.FILL_HORIZONTAL, + hSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + indent = 0, + ), + ) + + 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, + anchor = UIGridConstraints.ANCHOR_WEST, + hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK, + colSpan = 2, + indent = 0, + ), + ) + /** Project settings ------------------ */ if (isProjectSettingsAvailable(project)) { @@ -519,6 +577,7 @@ class SnykSettingsDialog( fill = UIGridConstraints.FILL_VERTICAL, hSizePolicy = 1, vSizePolicy = UIGridConstraints.SIZEPOLICY_WANT_GROW, + colSpan = 2, indent = 0, ), ) @@ -534,7 +593,7 @@ class SnykSettingsDialog( rootPanel.add( userExperiencePanel, baseGridConstraints( - row = 5, + row = 6, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_HORIZONTAL, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, @@ -575,7 +634,7 @@ class SnykSettingsDialog( rootPanel.add( executableSettingsPanel, baseGridConstraints( - row = 4, + row = 5, anchor = UIGridConstraints.ANCHOR_NORTHWEST, fill = UIGridConstraints.FILL_HORIZONTAL, hSizePolicy = UIGridConstraints.SIZEPOLICY_CAN_SHRINK or UIGridConstraints.SIZEPOLICY_CAN_GROW, @@ -756,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/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index f48549abc..d94b3db21 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,6 +43,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), + @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled(), @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 339b344b3..62c89f875 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -17,6 +17,7 @@ import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getWaitForResultsTimeout import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.pluginSettings +import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi @@ -405,6 +406,7 @@ class LanguageServerWrapper( integrationVersion = pluginInfo.integrationVersion, authenticationMethod = authMethod, enableSnykOSSQuickFixCodeActions = "true", + enableDeltaFindings = ps.isDeltaFindingsEnabled(), ) } From 9fc19764cab599d411b3e60e5879b4bde43f7f4c Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 6 Sep 2024 14:09:21 +0200 Subject: [PATCH 08/26] fix: settings layout (#603) * fix: settings layout * fix settings layout * fix: tests * fix: insets of net new * fix: clean ls caches on setting change between all/net-new issue scanning --- CHANGELOG.md | 6 +- src/main/kotlin/io/snyk/plugin/Utils.kt | 35 ++-- .../SnykApplicationSettingsStateService.kt | 8 +- .../SnykProjectSettingsConfigurable.kt | 5 + .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 190 +++++++++--------- .../SnykToolWindowSnykScanListenerLS.kt | 4 +- .../snyk/common/lsp/LanguageServerSettings.kt | 2 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 1 - .../SnykToolWindowSnykScanListenerLSTest.kt | 1 + 9 files changed, 134 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ca913e35..1de8ad615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Snyk Security Changelog -## [2.9.1] +## [2.10.0] ### Changed - save git folder config in settings - propagate Jetbrains determined runtime environment to language server @@ -8,12 +8,12 @@ - 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 in settings +- 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 delta scanning +- add dialog to choose reference branch when net new scanning - always display info nodes ### Fixes diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 4cbdc2e06..18881c6a1 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -252,32 +252,41 @@ fun getX509TrustManager(): X509TrustManager { 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/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 3ae24a2e8..b2db93677 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -101,8 +101,8 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().getAllForProject(project).forEach { val branchChooserTreeNode = ChooseBranchNode( diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index d94b3db21..a48a5223c 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,7 +43,7 @@ data class LanguageServerSettings( @SerializedName("enableSnykOSSQuickFixCodeActions") val enableSnykOSSQuickFixCodeActions: String? = null, @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), - @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled(), + @SerializedName("enableDeltaFindings") val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled().toString(), @SerializedName("folderConfigs") val folderConfigs: List = service().getAll().values.toList() ) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 62c89f875..42a898eff 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -406,7 +406,6 @@ class LanguageServerWrapper( integrationVersion = pluginInfo.integrationVersion, authenticationMethod = authMethod, enableSnykOSSQuickFixCodeActions = "true", - enableDeltaFindings = ps.isDeltaFindingsEnabled(), ) } 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 55c72350c..0a876e8e6 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -63,6 +63,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) + pluginSettings().setDeltaEnabled() } private fun mockScanIssues( From 7bda3b10eb59bfef382cc3a697a4917ac2188962 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 9 Sep 2024 10:05:31 +0200 Subject: [PATCH 09/26] fix: only create one annotation per finding, not one per fix (#604) * fix: only create one annotations and associate several fixes with it * chore: improve annotator performance call language server for code actions only once per range, do pre-sort before UI thread * refactoring: optimize usage of invokeLater * chore: cleanup unused code --- src/main/kotlin/io/snyk/plugin/Severity.kt | 9 - .../plugin/services/SnykTaskQueueService.kt | 5 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 60 ++-- .../SnykToolWindowSnykScanListenerLS.kt | 6 +- .../kotlin/snyk/common/AnnotatorCommon.kt | 5 +- .../snyk/common/annotator/SnykAnnotator.kt | 288 +++++++++--------- .../SnykIntentionActionBase.kt | 2 +- .../kotlin/snyk/common/lsp/RangeConverter.kt | 48 +++ .../snyk/common/lsp/SnykLanguageClient.kt | 18 +- .../container/ContainerBulkFileListener.kt | 2 +- .../kotlin/snyk/iac/IacBulkFileListener.kt | 5 +- .../snyk/iac/IgnoreButtonActionListener.kt | 5 +- src/main/kotlin/snyk/net/HttpClient.kt | 82 ----- .../kotlin/snyk/net/HttpLoggingInterceptor.kt | 32 -- src/test/kotlin/snyk/net/HttpClientTest.kt | 54 ---- 15 files changed, 240 insertions(+), 381 deletions(-) create mode 100644 src/main/kotlin/snyk/common/lsp/RangeConverter.kt delete mode 100644 src/main/kotlin/snyk/net/HttpClient.kt delete mode 100644 src/main/kotlin/snyk/net/HttpLoggingInterceptor.kt delete mode 100644 src/test/kotlin/snyk/net/HttpClientTest.kt 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/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index f8b0e1766..7ed43d59e 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -1,7 +1,6 @@ package io.snyk.plugin.services 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.fileEditor.FileDocumentManager @@ -158,7 +157,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("Container scan completed") - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }) } @@ -201,7 +200,7 @@ class SnykTaskQueueService(val project: Project) { } } logger.debug("IaC scan completed") - invokeLater { refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } }) } 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 41beb5c27..aec98ef48 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -179,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( @@ -214,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) } }, ) @@ -256,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) } } @@ -391,8 +391,8 @@ class SnykToolWindowPanel( ApplicationManager.getApplication().invokeLater { doCleanUi(true) - refreshAnnotationsForOpenFiles(project) } + refreshAnnotationsForOpenFiles(project) } private fun doCleanUi(reDisplayDescription: Boolean) { 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 68f3f837f..081bf42ff 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -77,22 +77,22 @@ class SnykToolWindowSnykScanListenerLS( this.snykToolWindowPanel.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displaySnykCodeResults(snykCachedResults?.currentSnykCodeResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) 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.triggerSelectionListeners = false val snykCachedResults = getSnykCachedResults(project) displayOssResults(snykCachedResults?.currentOSSResultsLS ?: emptyMap()) - refreshAnnotationsForOpenFiles(project) this.snykToolWindowPanel.triggerSelectionListeners = true } + refreshAnnotationsForOpenFiles(project) } override fun scanningError(snykScan: SnykScanParams) { 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/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index fa7a94879..ce9301a3f 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -17,16 +17,20 @@ 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 @@ -37,6 +41,7 @@ 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 @@ -45,8 +50,12 @@ import javax.swing.Icon private const val CODEACTION_TIMEOUT = 5000L +typealias SnykAnnotationInput = Pair>> +typealias SnykAnnotationList = List + abstract class SnykAnnotator(private val product: ProductType) : - ExternalAnnotator>, List>(), Disposable, DumbAware { + ExternalAnnotator(), Disposable, DumbAware { + private val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() val logger = logger() protected var disposed = false @@ -65,72 +74,92 @@ abstract class SnykAnnotator(private val product: ProductType) : val annotationSeverity: HighlightSeverity, val annotationMessage: String, val range: TextRange, - val intention: IntentionAction, + val intentionActions: MutableList = mutableListOf(), var gutterIconRenderer: GutterIconRenderer? = null ) // 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) + override fun collectInformation(file: PsiFile): SnykAnnotationInput? { + val map = getIssuesForFile(file) .filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) } - .distinctBy { it.id }) + .sortedByDescending { it.getSeverityAsEnum() } + .groupBy { it.range } + .toMap() + + return Pair(file, map) } - override fun doAnnotate(initialInfo: Pair>): List { + override fun doAnnotate(initialInfo: SnykAnnotationInput): SnykAnnotationList { if (disposed) return emptyList() - AnnotatorCommon.prepareAnnotate(initialInfo.first) if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList() - val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider() + 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() - val gutterIcons: MutableSet = mutableSetOf() + 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() } + } - initialInfo.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue -> - val textRange = textRange(initialInfo.first, issue.range) + 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() - if (textRange == null) { - logger.warn("Invalid range for issue: $issue") - return@forEach - } - if (!textRange.isEmpty) { - val detailAnnotation = SnykAnnotation( - issue, - highlightSeverity, - annotationMessage, - textRange, - ShowDetailsIntentionAction(annotationMessage, issue) - ) - - val gutterIconRenderer = - if (gutterIconEnabled && !gutterIcons.contains(textRange)) { - gutterIcons.add(textRange) - SnykShowDetailsGutterRenderer(detailAnnotation) - } else { - null - } - detailAnnotation.gutterIconRenderer = gutterIconRenderer - annotations.add(detailAnnotation) - - annotations.addAll( - getAnnotationsForCodeActions( - initialInfo, - issue, - highlightSeverity, - textRange - ) - ) - } + 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: List, + annotationResult: SnykAnnotationList, holder: AnnotationHolder, ) { if (disposed) return @@ -138,29 +167,51 @@ abstract class SnykAnnotator(private val product: ProductType) : annotationResult .forEach { annotation -> if (!annotation.range.isEmpty) { - val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) + val annoBuilder = holder + .newAnnotation(annotation.annotationSeverity, annotation.annotationMessage) .range(annotation.range) .textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum())) - .withFix(annotation.intention) + + annotation.intentionActions.forEach { + annoBuilder.withFix(it) + } + if (annotation.gutterIconRenderer != null) { annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation)) } + annoBuilder.create() } } } - private fun getAnnotationsForCodeActions( - initial: Pair>, + private fun getCodeActionsAsIntentionActions( issue: ScanIssue, - highlightSeverity: HighlightSeverity, - textRange: TextRange, - ): MutableList { - val addedAnnotationsList = mutableListOf() + 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(initial.first.virtualFile.toLanguageServerURL()), - issue.range, + TextDocumentIdentifier(file.toLanguageServerURL()), + range, CodeActionContext(emptyList()), ) val languageServer = LanguageServerWrapper.getInstance().languageServer @@ -169,29 +220,10 @@ abstract class SnykAnnotator(private val product: ProductType) : languageServer.textDocumentService .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() } catch (ignored: TimeoutException) { - logger.info("Timeout fetching code actions for issue: $issue") + logger.info("Timeout fetching code actions for range: $range") 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 - val codeActionAnnotation = SnykAnnotation( - issue, - highlightSeverity, - title, - textRange, - CodeActionIntention(issue, codeAction, product) - ) - addedAnnotationsList.add(codeActionAnnotation) - } - return addedAnnotationsList + return codeActions } private fun getLineMarkerProvider(): SnykLineMarkerProvider { @@ -222,87 +254,51 @@ abstract class SnykAnnotator(private val product: ProductType) : ?.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 + class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { + override fun equals(other: Any?): Boolean { + return annotation == other } - } -} -class SnykShowDetailsGutterRenderer(val annotation: SnykAnnotation) : GutterIconRenderer() { - override fun equals(other: Any?): Boolean { - return annotation == other - } - - override fun hashCode(): Int { - return annotation.hashCode() - } + override fun hashCode(): Int { + return annotation.hashCode() + } - override fun getIcon(): Icon { - return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) - } + override fun getIcon(): Icon { + return SnykIcons.getSeverityIcon(annotation.issue.getSeverityAsEnum()) + } - override fun getClickAction(): AnAction? { - if (annotation.intention !is ShowDetailsIntentionAction) return null - return getShowDetailsNavigationAction(annotation.intention) - } + 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) + 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 getTooltipText(): String { + return annotation.annotationMessage + } - override fun getAccessibleName(): String { - return annotation.annotationMessage - } + override fun getAccessibleName(): String { + return annotation.annotationMessage + } - override fun isNavigateAction(): Boolean { - return true - } + override fun isNavigateAction(): Boolean { + return true + } - override fun isDumbAware(): Boolean { - return true + override fun isDumbAware(): Boolean { + return true + } } } 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/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 29f192460..e8af51dd1 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -19,11 +19,9 @@ import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService 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 @@ -187,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 } 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/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()) } - } - } -} From 0f2595caf8c6b88f78a425bdc9d418a060d45f9c Mon Sep 17 00:00:00 2001 From: Arvyd Date: Mon, 9 Sep 2024 14:19:53 +0200 Subject: [PATCH 10/26] chore: fixed parser of the Readme according to the latest state of the docs, to properly find the plugin description --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 3a34b1f5b3d6785348d973d930c6fd0b88f54334 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 12:40:26 +0000 Subject: [PATCH 11/26] docs: synchronizing README from snyk/user-docs --- README.md | 80 +++++++++++++++++-------------------------------------- 1 file changed, 24 insertions(+), 56 deletions(-) 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

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. From fc50f9b324f083f5365270d73bd045cf21635f3a Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 9 Sep 2024 16:50:54 +0200 Subject: [PATCH 12/26] feat: add capability to display hover (#605) * feat: add capability to display hover This is disabled by default and can be enabled in the IntelliJ registry with the key snyk.documentationHoversEnabled. * fix: minimize read lock during code vision calculation, improve annotations --- CHANGELOG.md | 1 + src/main/kotlin/io/snyk/plugin/Utils.kt | 26 +----- .../ui/toolwindow/SnykPluginDisposable.kt | 18 ++-- .../SnykToolWindowSnykScanListenerLS.kt | 4 +- .../snyk/common/annotator/SnykAnnotator.kt | 11 ++- .../snyk/common/lsp/LSCodeVisionProvider.kt | 67 +++++++-------- .../lsp/LSDocumentationTargetProvider.kt | 85 +++++++++++++++++++ .../snyk/common/lsp/LanguageServerWrapper.kt | 12 +-- src/main/resources/META-INF/plugin.xml | 18 ++-- 9 files changed, 159 insertions(+), 83 deletions(-) create mode 100644 src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 1de8ad615..48ddba793 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - 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 ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 18881c6a1..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,25 +230,6 @@ 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? { return if (!virtualFile.isValid || project.isDisposed) { null 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..90bc96daf 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) } } @@ -46,7 +57,4 @@ class SnykPluginDisposable : Disposable, AppLifecycleListener { // do nothing } } - - override fun dispose() = Unit - } 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 081bf42ff..fc6abc2dd 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -117,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 diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index ce9301a3f..f482120a5 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -48,7 +48,7 @@ import java.util.concurrent.TimeoutException import javax.swing.Icon -private const val CODEACTION_TIMEOUT = 5000L +private const val CODEACTION_TIMEOUT = 10L typealias SnykAnnotationInput = Pair>> typealias SnykAnnotationList = List @@ -78,8 +78,9 @@ abstract class SnykAnnotator(private val product: ProductType) : var gutterIconRenderer: GutterIconRenderer? = null ) - // overrides needed for the Annotator to invoke apply(). We don't do anything here 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() } @@ -111,9 +112,7 @@ abstract class SnykAnnotator(private val product: ProductType) : logger.warn("Invalid range for range: $textRange") return@forEach } - annotations.addAll( - doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions) - ) + annotations.addAll(doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions)) } return annotations.sortedByDescending { it.issue.getSeverityAsEnum() } } @@ -218,7 +217,7 @@ abstract class SnykAnnotator(private val product: ProductType) : val codeActions = try { languageServer.textDocumentService - .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList() + .codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.SECONDS) ?: emptyList() } catch (ignored: TimeoutException) { logger.info("Timeout fetching code actions for range: $range") emptyList() 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..e59af9db0 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -0,0 +1,85 @@ +@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 computeDocumentationHint(): String? { + val htmlText = convertMarkdownToHtml(hover.contents.right.value) + if (htmlText.isEmpty()) { + return null + } + return htmlText.split("\n")[0] + } + + override fun computeDocumentation(): DocumentationResult? { + val htmlText = convertMarkdownToHtml(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/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 42a898eff..34908e7ef 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -561,8 +561,13 @@ class LanguageServerWrapper( return "" } - companion object { + override fun dispose() { + disposed = true + shutdown() + } + + companion object { private var instance: LanguageServerWrapper? = null fun getInstance() = instance ?: LanguageServerWrapper().also { @@ -571,10 +576,5 @@ class LanguageServerWrapper( } } - override fun dispose() { - disposed = true - shutdown() - } - } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d2fc3bd25..75c1e420a 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,9 +19,9 @@ com.intellij.modules.xml - + + - - - + + + From e5391aba163a95464ed0af221fdf41f830397fb3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 11 Sep 2024 13:09:50 +0200 Subject: [PATCH 13/26] fix: show hand icon, refactor info node text construction (#609) * fix: show hand icon, refactor info node text construction * fix: tests * fix: tests --- .../toolwindow/SnykToolWindowSnykScanListenerLS.kt | 7 ++++--- .../snyk/plugin/extensions/SnykControllerImplTest.kt | 4 ++-- .../plugin/ui/BranchChooserComboBoxDialogTest.kt | 5 ++++- .../SnykToolWindowSnykScanListenerLSTest.kt | 6 +++--- .../snyk/container/ContainerBulkFileListenerTest.kt | 12 +++++++++--- 5 files changed, 22 insertions(+), 12 deletions(-) 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 fc6abc2dd..ae02db22e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -286,11 +286,12 @@ class SnykToolWindowSnykScanListenerLS( val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } if (issuesCount != 0) { - text = if (issuesCount == 1) { - "$issuesCount vulnerability found by Snyk" + val plural = if (issuesCount == 1) { + "y" } else { - "✋ $issuesCount vulnerabilities found by Snyk" + "ies" } + text = "✋ $issuesCount vulnerabilit$plural found by Snyk" if (pluginSettings().isGlobalIgnoresFeatureEnabled) { text += ", $ignoredIssuesCount ignored" } 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/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt index 17c14e897..c55631bb7 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -3,12 +3,15 @@ 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.toVirtualFile import okio.Path.Companion.toPath import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.services.LanguageServer @@ -30,10 +33,10 @@ class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { unmockkAll() folderConfig = FolderConfig(project.basePath.toString(), "testBranch") service().addFolderConfig(folderConfig) - project.basePath?.let { service().addTrustedPath(it.toNioPathOrNull()!!) } val languageServerWrapper = LanguageServerWrapper.getInstance() languageServerWrapper.isInitialized = true languageServerWrapper.languageServer = lsMock + project.basePath?.let { service().addTrustedPath(it.toPath().parent!!.toNioPath()) } cut = BranchChooserComboBoxDialog(project) } 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 0a876e8e6..967ddd64b 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -148,8 +148,8 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { 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(), - "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( rootTreeNode.children().toList()[5].toString(), @@ -188,12 +188,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { 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( + "✋ 1 vulnerability found by Snyk, 0 ignored", rootTreeNode.children().toList()[4].toString(), - "1 vulnerability found by Snyk, 0 ignored", ) TestCase.assertEquals( - rootTreeNode.children().toList()[5].toString(), "⚡ 1 vulnerabilities can be fixed automatically", + rootTreeNode.children().toList()[5].toString(), ) } diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 6c05e2797..0e2ec34a1 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -1,6 +1,9 @@ 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.components.service import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.vfs.VirtualFile @@ -28,6 +31,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 @@ -70,12 +74,14 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { setUpContainerTest() val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) - VirtualFileManager.getInstance().syncRefresh() var virtualFile: VirtualFile? = null - await().atMost(2, TimeUnit.SECONDS).until { + invokeLater { + VirtualFileManager.getInstance().syncRefresh() virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) - virtualFile?.isValid ?: false } + PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + await().timeout(5, TimeUnit.SECONDS).until { virtualFile?.isValid ?: false } + ApplicationManager.getApplication().runWriteAction { val file = PsiManager.getInstance(project).findFile(virtualFile!!) From 4de8e1166f798bcc816341b5bdc2b2624d61d5f0 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 11 Sep 2024 15:58:19 +0200 Subject: [PATCH 14/26] * feat: enable documentation hovers * docs: update changelog --- CHANGELOG.md | 1 + .../snyk/common/lsp/LSDocumentationTargetProvider.kt | 10 +--------- .../kotlin/snyk/common/lsp/LanguageServerSettings.kt | 2 ++ src/main/resources/META-INF/plugin.xml | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48ddba793..6a954a600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - 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 ### Fixes - add name to code vision provider diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt index e59af9db0..66fb4ae47 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -53,16 +53,8 @@ class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { } inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { - override fun computeDocumentationHint(): String? { - val htmlText = convertMarkdownToHtml(hover.contents.right.value) - if (htmlText.isEmpty()) { - return null - } - return htmlText.split("\n")[0] - } - override fun computeDocumentation(): DocumentationResult? { - val htmlText = convertMarkdownToHtml(hover.contents.right.value) + val htmlText = hover.contents.right.value if (htmlText.isEmpty()) { return null } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt index a48a5223c..8b57c0b1d 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt @@ -43,6 +43,8 @@ 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() ) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 75c1e420a..d5e39c49b 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -43,7 +43,7 @@ defaultValue="720000" description="Snyk timeout (milliseconds) to wait for results during scan"/> From f25499df2bb4ac75d931dcad8eb826854e1c9e04 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Thu, 12 Sep 2024 11:26:19 +0200 Subject: [PATCH 15/26] fix: propagate intellij environment to language server --- src/main/kotlin/snyk/common/EnvironmentHelper.kt | 3 +++ src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/EnvironmentHelper.kt b/src/main/kotlin/snyk/common/EnvironmentHelper.kt index 3b763e98a..1f66c2b28 100644 --- a/src/main/kotlin/snyk/common/EnvironmentHelper.kt +++ b/src/main/kotlin/snyk/common/EnvironmentHelper.kt @@ -1,5 +1,6 @@ package snyk.common +import com.intellij.util.EnvironmentUtil import com.intellij.util.net.HttpConfigurable import io.snyk.plugin.pluginSettings import snyk.pluginInfo @@ -11,6 +12,8 @@ object EnvironmentHelper { environment: MutableMap, apiToken: String, ) { + // first of all, use IntelliJ environment tool, to spice up env + environment.putAll(EnvironmentUtil.getEnvironmentMap()) val endpoint = getEndpointUrl() val oauthEnabledEnvVar = "INTERNAL_SNYK_OAUTH_ENABLED" diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 39a723749..738908310 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -19,7 +19,6 @@ import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.isSnykOSSLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.toLanguageServerURL -import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -132,7 +131,7 @@ 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) From 766bb599f3e0a04887654884f7d53ea63b1f5710 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Thu, 12 Sep 2024 11:29:19 +0200 Subject: [PATCH 16/26] docs: update docs --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb4dc9815..4f51e88f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Snyk Security Changelog +## [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 - Updated the language server protocol version to 14 to support new communication model. From 0aecb22ea7b4e8b4f05a3c7c9dd5f5bb937b4786 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 08:26:52 +0200 Subject: [PATCH 17/26] fix: don't display shutdown exceptions, auto-scan on startup --- .../io/snyk/plugin/SnykPostStartupActivity.kt | 2 +- .../plugin/extensions/SnykControllerImpl.kt | 2 +- .../plugin/services/SnykTaskQueueService.kt | 6 ++--- .../services/download/CliDownloaderService.kt | 2 +- .../plugin/ui/actions/SnykRunScanAction.kt | 2 +- .../ui/settings/IssueViewOptionsPanel.kt | 4 ++-- .../ui/toolwindow/SnykPluginDisposable.kt | 4 ++-- .../ui/toolwindow/SnykToolWindowPanel.kt | 2 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 23 +++++++++++++------ .../services/SnykTaskQueueServiceTest.kt | 11 ++++----- .../SnykToolWindowPanelIntegTest.kt | 12 +++++----- .../ui/toolwindow/SnykToolWindowPanelTest.kt | 5 ++-- 12 files changed, 40 insertions(+), 35 deletions(-) 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/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/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 7ed43d59e..b23b9c75f 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -89,7 +89,7 @@ class SnykTaskQueueService(val project: Project) { } } - fun scan(isStartup: Boolean) { + fun scan() { taskQueue.run(object : Task.Backgroundable(project, "Snyk: initializing...", true) { override fun run(indicator: ProgressIndicator) { if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project)) return @@ -101,9 +101,7 @@ class SnykTaskQueueService(val project: Project) { waitUntilCliDownloadedIfNeeded() indicator.checkCanceled() - if (!isStartup) { - LanguageServerWrapper.getInstance().sendScanCommand(project) - } + LanguageServerWrapper.getInstance().sendScanCommand(project) if (settings.iacScanEnabled) { if (!isSnykIaCLSEnabled()) { diff --git a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt index a13944600..65c4a4b34 100644 --- a/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/download/CliDownloaderService.kt @@ -73,7 +73,7 @@ class SnykCliDownloaderService { try { if (languageServerWrapper.isInitialized) { try { - languageServerWrapper.shutdown().get(2, TimeUnit.SECONDS) + languageServerWrapper.shutdown() } catch (e: RuntimeException) { logger() .warn("Language server shutdown for download took too long, couldn't shutdown", e) 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 90bc96daf..eba4422fc 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykPluginDisposable.kt @@ -44,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 } @@ -52,7 +52,7 @@ 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 } 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 aec98ef48..0c7bd392d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -449,7 +449,7 @@ class SnykToolWindowPanel( } private fun triggerScan() { - getSnykTaskQueueService(project)?.scan(false) + getSnykTaskQueueService(project)?.scan() } fun displayAuthPanel() { diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index c5487af1c..09c5984ca 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -157,17 +157,26 @@ class LanguageServerWrapper( } } - fun shutdown(): Future<*> = - executorService.submit { - if (::process.isInitialized && process.isAlive) { - languageServer.shutdown().get(1, TimeUnit.SECONDS) + fun shutdown() { + try { + val shouldShutdown = lsIsAlive() + executorService.submit { if (shouldShutdown) languageServer.shutdown().get(1, TimeUnit.SECONDS) } + } catch (ignored: TimeoutException) { + // we don't care + } finally { + try { languageServer.exit() - if (process.isAlive) { - process.destroyForcibly() - } + } catch (ignore: Exception) { + // do nothing + } finally { + if (lsIsAlive()) process.destroyForcibly() } } + } + + private fun lsIsAlive() = ::process.isInitialized && process.isAlive + private fun determineWorkspaceFolders(): List { val workspaceFolders = mutableSetOf() ProjectManager.getInstance().openProjects.forEach { 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/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 From 037df5c0573b1158830e4c502c71dc82860caf65 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 09:34:38 +0200 Subject: [PATCH 18/26] fix: shut up lsp4j logger during shutdown, show errors & warnings from LSP --- .../ui/SnykBalloonNotificationHelper.kt | 4 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 38 +++++++++++++------ .../snyk/common/lsp/SnykLanguageClient.kt | 8 ++-- 3 files changed, 31 insertions(+), 19 deletions(-) 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/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 09c5984ca..d6e85494d 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -17,7 +17,6 @@ import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getWaitForResultsTimeout import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.pluginSettings -import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import kotlinx.coroutines.DelicateCoroutinesApi @@ -42,6 +41,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 @@ -62,10 +62,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.* import kotlin.io.path.exists private const val INITIALIZATION_TIMEOUT = 20L @@ -134,22 +134,32 @@ class LanguageServerWrapper( 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) } + 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() @@ -158,21 +168,25 @@ class LanguageServerWrapper( } 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: TimeoutException) { + } catch (ignored: Exception) { // we don't care } finally { try { - languageServer.exit() + if (lsIsAlive()) languageServer.exit() } catch (ignore: Exception) { // do nothing } finally { if (lsIsAlive()) process.destroyForcibly() } + lsp4jLogger.level = previousLSP4jLogLevel } - } private fun lsIsAlive() = ::process.isInitialized && process.isAlive diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index e8af51dd1..b6c99ba97 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -460,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) From 6c2ca84318da1926afc8aa012f46a2e5497fdc29 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 13:44:09 +0200 Subject: [PATCH 19/26] fix: minor logging / api usage improvements --- .../kotlin/io/snyk/plugin/SnykProjectManagerListener.kt | 6 ++++-- src/main/kotlin/snyk/trust/TrustedProjects.kt | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) 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/snyk/trust/TrustedProjects.kt b/src/main/kotlin/snyk/trust/TrustedProjects.kt index 508816c03..884e6e30c 100644 --- a/src/main/kotlin/snyk/trust/TrustedProjects.kt +++ b/src/main/kotlin/snyk/trust/TrustedProjects.kt @@ -4,6 +4,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 +59,7 @@ private fun confirmScanningUntrustedProject(project: Project): ScanUntrustedProj var choice = ScanUntrustedProjectChoice.CANCEL - invokeAndWaitIfNeeded { + runInEdt { val result = MessageDialogBuilder .yesNo(title, message) .icon(Messages.getWarningIcon()) From 9bc973af6e2c007289bff863944745b2444b46c8 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 14:30:09 +0200 Subject: [PATCH 20/26] fix: tests --- src/main/kotlin/snyk/trust/TrustedProjects.kt | 1 - src/test/kotlin/io/snyk/plugin/TestUtils.kt | 6 +++++- .../SnykToolWindowSnykScanListenerLSTest.kt | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/snyk/trust/TrustedProjects.kt b/src/main/kotlin/snyk/trust/TrustedProjects.kt index 884e6e30c..96cd62c79 100644 --- a/src/main/kotlin/snyk/trust/TrustedProjects.kt +++ b/src/main/kotlin/snyk/trust/TrustedProjects.kt @@ -3,7 +3,6 @@ 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 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/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt index 967ddd64b..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,5 +1,6 @@ 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 @@ -11,10 +12,12 @@ 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.common.annotator.SnykCodeAnnotator @@ -23,9 +26,12 @@ 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 @@ -53,10 +59,11 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { file = myFixture.copyFileToProject(fileName) psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } + val contentRootPaths = project.getContentRootPaths() service() .addFolderConfig( FolderConfig( - project.getContentRootPaths().first().toAbsolutePath().toString(), "main" + contentRootPaths.first().toAbsolutePath().toString(), "main" ) ) snykToolWindowPanel = SnykToolWindowPanel(project) @@ -64,6 +71,12 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) pluginSettings().setDeltaEnabled() + contentRootPaths.forEach { service().addTrustedPath(it.root.absolutePathString())} + } + + override fun tearDown() { + super.tearDown() + unmockkAll() } private fun mockScanIssues( From 83058c69bbd78792293bb530dd3829cdb6be76c5 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 14:31:48 +0200 Subject: [PATCH 21/26] fix: container test --- .../kotlin/snyk/container/ContainerBulkFileListenerTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt index 0e2ec34a1..cd5d769f5 100644 --- a/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt +++ b/src/test/kotlin/snyk/container/ContainerBulkFileListenerTest.kt @@ -4,6 +4,7 @@ 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 @@ -75,8 +76,8 @@ class ContainerBulkFileListenerTest : BasePlatformTestCase() { val path = createNewFileInProjectRoot().toPath() Files.write(path, "\n".toByteArray(Charsets.UTF_8)) var virtualFile: VirtualFile? = null - invokeLater { - VirtualFileManager.getInstance().syncRefresh() + VirtualFileManager.getInstance().syncRefresh() + runInEdt { virtualFile = VirtualFileManager.getInstance().findFileByNioPath(path) } PlatformTestUtil.dispatchAllEventsInIdeEventQueue() From ad50ff0964e5f6774e01b69afe8558da0783c3e9 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 14:34:45 +0200 Subject: [PATCH 22/26] docs: update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 962150580..0640478e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ - 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 From 8938f6aedef98839543a38e0964cba7e802b0abe Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 15:16:03 +0200 Subject: [PATCH 23/26] fix: add trust path --- src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 2 +- .../io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index d6e85494d..2f8112173 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -65,7 +65,7 @@ import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import java.util.concurrent.locks.ReentrantLock -import java.util.logging.Logger.* +import java.util.logging.Logger.getLogger import kotlin.io.path.exists private const val INITIALIZATION_TIMEOUT = 20L diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt index c55631bb7..9a7b000ff 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -11,6 +11,7 @@ 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 @@ -21,11 +22,13 @@ 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) - lateinit var folderConfig: FolderConfig + private lateinit var folderConfig: FolderConfig lateinit var cut: BranchChooserComboBoxDialog override fun setUp(): Unit { @@ -33,6 +36,7 @@ class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { 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 From ee670d7ffbb2a0e5f3005b01af821d9729798627 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 15:44:33 +0200 Subject: [PATCH 24/26] fix: add trust path to test --- .../snyk/plugin/ui/toolwindow/settings/ScanTypesPanelTest.kt | 5 +++++ 1 file changed, 5 insertions(+) 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() } From 3ed21319753acbdea810484ddd679c4636000fcc Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 16:38:20 +0200 Subject: [PATCH 25/26] fix: container service test --- src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt b/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt index f0d70d0b3..879d72743 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,7 +18,9 @@ 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() { @@ -51,6 +55,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 From 451ce4c574d14e0ae1c06f98d2fab2ffed896c8b Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 13 Sep 2024 16:38:42 +0200 Subject: [PATCH 26/26] fix: warning --- src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt b/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt index 879d72743..2756f1941 100644 --- a/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt +++ b/src/test/kotlin/snyk/container/ContainerServiceIntegTest.kt @@ -22,7 +22,6 @@ 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