From 9271c6f1492842de087c6e277ab0ef5668b84022 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 07:54:09 +0200 Subject: [PATCH 01/51] fix: shut up lsp4j logger during shutdown --- src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 2f8112173..b39dfc0f7 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -42,6 +42,7 @@ 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.jsonrpc.json.StreamMessageProducer import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.LanguageServer import org.jetbrains.concurrency.runAsync @@ -65,6 +66,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.Level import java.util.logging.Logger.getLogger import kotlin.io.path.exists @@ -170,8 +172,11 @@ 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 messageProducerLogger = getLogger(StreamMessageProducer::class.java.name) + val previousMessageProducerLevel = messageProducerLogger.level val previousLSP4jLogLevel = lsp4jLogger.level - lsp4jLogger.level = java.util.logging.Level.OFF + lsp4jLogger.level = Level.OFF + messageProducerLogger.level = Level.OFF try { val shouldShutdown = lsIsAlive() executorService.submit { if (shouldShutdown) languageServer.shutdown().get(1, TimeUnit.SECONDS) } @@ -186,6 +191,7 @@ class LanguageServerWrapper( if (lsIsAlive()) process.destroyForcibly() } lsp4jLogger.level = previousLSP4jLogLevel + messageProducerLogger.level = previousMessageProducerLevel } } From e8f8295523d9c25ee459f4c8265665c9c1635a6c Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 09:21:29 +0200 Subject: [PATCH 02/51] feat: add issue vulnerability count / inline value support --- src/main/kotlin/io/snyk/plugin/Utils.kt | 2 + .../editor/LineEndingEditorFactoryListener.kt | 129 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 132 insertions(+) create mode 100644 src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index e6c316a02..958eab791 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -1,4 +1,5 @@ @file:JvmName("UtilsKt") +@file:Suppress("unused") package io.snyk.plugin @@ -427,6 +428,7 @@ fun Project.getContentRootPaths(): SortedSet { } fun Project.getContentRootVirtualFiles(): Set { + if (this.isDisposed) return emptySet() var contentRoots = ProjectRootManager.getInstance(this).contentRoots if (contentRoots.isEmpty()) { // this should cover for the case when no content roots are configured, e.g. in rider diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt new file mode 100644 index 000000000..02ed1e893 --- /dev/null +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -0,0 +1,129 @@ +package snyk.common.editor + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.LogLevel +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LineExtensionInfo +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.toLanguageServerURL +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.InlineValueContext +import org.eclipse.lsp4j.InlineValueParams +import org.eclipse.lsp4j.InlineValueText +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.messages.Either +import snyk.common.lsp.LanguageServerWrapper +import java.util.concurrent.TimeUnit + +class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + override fun dispose() { + disposed = true + } + + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + + override fun editorCreated(event: EditorFactoryEvent) { + if (disposed) return + val editor = event.editor + + if (editor.isOneLineMode) { + return + } + + val project = editor.project + if (project == null || project.isDisposed) { + return + } + + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) + if (psiFile == null || psiFile.virtualFile == null || psiFile is PsiCompiledElement) { + return + } + + if (editor !is EditorEx) { + return + } + + editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) + } + + class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) + + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } + + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + override fun dispose() { + disposed = true + } + + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + + fun getLineExtension(line: Int): MutableCollection { + if (disposed || !LanguageServerWrapper.getInstance() + .ensureLanguageServerInitialized() + ) return mutableSetOf() + + val document = editor.document + val lineEndOffset = document.getLineEndOffset(line) + val lineStartOffset = document.getLineStartOffset(line) + val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) + val ctx = InlineValueContext(0, range) + val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) + + val inlineValue = LanguageServerWrapper.getInstance().languageServer.textDocumentService.inlineValue(params) + @Suppress("USELESS_CAST") val text = try { + val result = inlineValue.get(120, TimeUnit.SECONDS) + if (result != null && result.isNotEmpty()) { + // this supposedly unneeded cast is needed as the conversion in lsp4j does not work correctly + val firstInlineValue = (result as List>)[0] + firstInlineValue.left.text + } else { + "" + } + } catch (ignored: Exception) { + // ignore error + "" + } + return mutableListOf(LineExtensionInfo("\t\t$text", attributes)) + } + + } +} + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d5e39c49b..e2c8a9936 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -56,6 +56,7 @@ + From f7c344290c24f04c1c61306279b8be65d479ff7a Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 09:22:12 +0200 Subject: [PATCH 03/51] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0640478e2..a84c21128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - always display info nodes - add option in IntelliJ registry to display tooltips with issue information - display documentation info when hovering over issue +- add inline value support for display of vulnerability count ### Fixes - add name to code vision provider From 4f08fed8b969caa987b5712d5f35ecfae648083b Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:11:06 +0200 Subject: [PATCH 04/51] fix: early exit editor painting if no issues known --- .../editor/LineEndingEditorFactoryListener.kt | 46 +++++++++++-------- .../snyk/common/lsp/LanguageServerWrapper.kt | 2 +- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 02ed1e893..9b7aa234c 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import org.eclipse.lsp4j.InlineValueContext @@ -69,15 +70,12 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) } - class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { - private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT - val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) - val logger = Logger.getInstance(this::class.java).apply { - // tie log level to language server log level - val languageServerWrapper = LanguageServerWrapper.getInstance() - if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) - if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + companion object { + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) + val ctx = InlineValueContext(0, Range()) } private var disposed = false @@ -86,6 +84,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { } fun isDisposed() = disposed + override fun dispose() { disposed = true } @@ -94,25 +93,36 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { Disposer.register(SnykPluginDisposable.getInstance(), this) } + val project = editor.project + val document = editor.document + val snykCachedResults = project?.let { getSnykCachedResults(it) } + val languageServerWrapper = LanguageServerWrapper.getInstance() + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } + fun getLineExtension(line: Int): MutableCollection { - if (disposed || !LanguageServerWrapper.getInstance() - .ensureLanguageServerInitialized() + // only OSS has inline values right now + val hasResults = snykCachedResults?.currentOSSResultsLS?.isNotEmpty() ?: false + if (disposed + || !languageServerWrapper.isInitialized + || !hasResults ) return mutableSetOf() - val document = editor.document val lineEndOffset = document.getLineEndOffset(line) val lineStartOffset = document.getLineStartOffset(line) val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) - val ctx = InlineValueContext(0, range) val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) - - val inlineValue = LanguageServerWrapper.getInstance().languageServer.textDocumentService.inlineValue(params) - @Suppress("USELESS_CAST") val text = try { - val result = inlineValue.get(120, TimeUnit.SECONDS) + val inlineValue = languageServerWrapper.languageServer.textDocumentService.inlineValue(params) + val text = try { + val result = inlineValue.get(5, TimeUnit.MILLISECONDS) if (result != null && result.isNotEmpty()) { // this supposedly unneeded cast is needed as the conversion in lsp4j does not work correctly - val firstInlineValue = (result as List>)[0] - firstInlineValue.left.text + val eitherList = result as List> + val firstInlineValue = eitherList[0] + firstInlineValue.left.text ?: "" } else { "" } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index b39dfc0f7..39fcee59d 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -369,7 +369,7 @@ class LanguageServerWrapper( val param = ExecuteCommandParams() param.command = COMMAND_GET_FEATURE_FLAG_STATUS param.arguments = listOf(featureFlag) - val result = languageServer.workspaceService.executeCommand(param).get(10, TimeUnit.SECONDS) + val result = languageServer.workspaceService.executeCommand(param).get(20, TimeUnit.SECONDS) val resultMap = result as? Map<*, *> val ok = resultMap?.get("ok") as? Boolean ?: false From 43b90cb1c4b4a7857b12e90a29fbe1c359147f7f Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:13:01 +0200 Subject: [PATCH 05/51] fix: catch timeout exception from documentation provider --- .../common/lsp/LSDocumentationTargetProvider.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt index 66fb4ae47..d19f12cb9 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -19,6 +19,7 @@ import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.TextDocumentIdentifier import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException class SnykDocumentationTargetPointer(private val documentationTarget: DocumentationTarget) : Pointer { @@ -46,10 +47,14 @@ class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { 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)) + try { + val hover = + languageServerWrapper.languageServer.textDocumentService.hover(hoverParams).get(5, TimeUnit.SECONDS) + if (hover == null || hover.contents.right.value.isEmpty()) return mutableListOf() + return mutableListOf(SnykDocumentationTarget(hover)) + } catch (ignored: TimeoutException) { + return mutableListOf() + } } inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { From abe9170a7cf90ca61f7e2c9b56572d274928086b Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:25:46 +0200 Subject: [PATCH 06/51] fix: check cache on file level --- .../editor/LineEndingEditorFactoryListener.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 9b7aa234c..4b73b40bd 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -14,9 +14,9 @@ import com.intellij.openapi.editor.event.EditorFactoryListener import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.SnykFile import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable @@ -59,7 +59,8 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { } val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) - if (psiFile == null || psiFile.virtualFile == null || psiFile is PsiCompiledElement) { + val virtualFile = psiFile?.virtualFile + if (psiFile == null || virtualFile == null || psiFile is PsiCompiledElement) { return } @@ -67,13 +68,15 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { return } - editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) + val snykFile = SnykFile(project, virtualFile) + + editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, snykFile)::getLineExtension) } - class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + class EditorLineEndingProvider(val editor: Editor, val snykFile: SnykFile) : Disposable { companion object { - private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.INLAY_TEXT_WITHOUT_BACKGROUND val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) val ctx = InlineValueContext(0, Range()) } @@ -105,7 +108,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { fun getLineExtension(line: Int): MutableCollection { // only OSS has inline values right now - val hasResults = snykCachedResults?.currentOSSResultsLS?.isNotEmpty() ?: false + val hasResults = snykCachedResults?.currentOSSResultsLS?.get(snykFile)?.isNotEmpty() ?: false if (disposed || !languageServerWrapper.isInitialized || !hasResults @@ -114,7 +117,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { val lineEndOffset = document.getLineEndOffset(line) val lineStartOffset = document.getLineStartOffset(line) val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) - val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) + val params = InlineValueParams(TextDocumentIdentifier(snykFile.virtualFile.toLanguageServerURL()), range, ctx) val inlineValue = languageServerWrapper.languageServer.textDocumentService.inlineValue(params) val text = try { val result = inlineValue.get(5, TimeUnit.MILLISECONDS) From c99551a638f122dd627c856047133f63e20458d0 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:29:23 +0200 Subject: [PATCH 07/51] fix: remove second tab from inline value display --- .../snyk/common/editor/LineEndingEditorFactoryListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 4b73b40bd..0d72f2101 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -133,7 +133,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { // ignore error "" } - return mutableListOf(LineExtensionInfo("\t\t$text", attributes)) + return mutableListOf(LineExtensionInfo("\t$text", attributes)) } } From f968446f2f8da7b61bbb06efd56a5e1524b481aa Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:42:44 +0200 Subject: [PATCH 08/51] fix: don't scan double on branch choosing --- src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt index 547bc2401..042d21376 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -68,7 +68,6 @@ class BranchChooserComboBoxDialog(val project: Project) : DialogWrapper(true) { } runAsync { LanguageServerWrapper.getInstance().updateConfiguration() - LanguageServerWrapper.getInstance().sendScanCommand(project) } } From c52b51bf55ac94ab49fcc2fd333c14c6dd19e379 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:45:56 +0200 Subject: [PATCH 09/51] fix: refresh feature flags once before scanning --- src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 39fcee59d..58ba0dbb1 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -188,7 +188,7 @@ class LanguageServerWrapper( } catch (ignore: Exception) { // do nothing } finally { - if (lsIsAlive()) process.destroyForcibly() + if (lsIsAlive()) process.destroyForcibly() } lsp4jLogger.level = previousLSP4jLogLevel messageProducerLogger.level = previousMessageProducerLevel @@ -342,8 +342,8 @@ class LanguageServerWrapper( fun sendScanCommand(project: Project) { if (!ensureLanguageServerInitialized()) return DumbService.getInstance(project).runWhenSmart { + refreshFeatureFlags() getTrustedContentRoots(project).forEach { - refreshFeatureFlags() sendFolderScanCommand(it.path, project) } } From 781146725288508bca8919627fb0b023c2ade4db Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 11:29:38 +0200 Subject: [PATCH 10/51] fix: flickering when rendering html --- .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 24 ++++++++++++++----- .../snyk/common/lsp/LanguageServerWrapper.kt | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt index d5998e20d..273027b5e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -3,12 +3,15 @@ package io.snyk.plugin.ui.jcef import com.intellij.ui.jcef.JBCefApp import com.intellij.ui.jcef.JBCefBrowser import com.intellij.ui.jcef.JBCefBrowserBuilder +import com.intellij.ui.jcef.JBCefClient import org.cef.handler.CefLoadHandlerAdapter import java.awt.Component typealias LoadHandlerGenerator = (jbCefBrowser: JBCefBrowser) -> CefLoadHandlerAdapter object JCEFUtils { + private val jbCefPair : Pair? = null + fun getJBCefBrowserComponentIfSupported( html: String, loadHandlerGenerators: List, @@ -16,12 +19,7 @@ object JCEFUtils { if (!JBCefApp.isSupported()) { return null } - val cefClient = JBCefApp.getInstance().createClient() - cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) - val jbCefBrowser = - JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) - .setMouseWheelEventEnable(true).build() - jbCefBrowser.setOpenLinksInExternalBrowser(true) + val (cefClient, jbCefBrowser) = getBrowser() for (loadHandlerGenerator in loadHandlerGenerators) { val loadHandler = loadHandlerGenerator(jbCefBrowser) @@ -31,4 +29,18 @@ object JCEFUtils { return jbCefBrowser.component } + + private fun getBrowser(): Pair { + if (jbCefPair != null) { + return jbCefPair + } + val cefClient = JBCefApp.getInstance().createClient() + cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) + val jbCefBrowser = + JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) + .setMouseWheelEventEnable(true).build() + jbCefBrowser.setOpenLinksInExternalBrowser(true) + val jbCefPair = Pair(cefClient, jbCefBrowser) + return jbCefPair + } } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 58ba0dbb1..916456fed 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -401,6 +401,7 @@ class LanguageServerWrapper( languageServer.workspaceService.executeCommand(param) } catch (ignored: Exception) { // do nothing to not break UX for analytics + // TODO review } } From b5c4ec57c2b7ad9bc77aefcefdce0a11c98b3ee2 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 13:02:50 +0200 Subject: [PATCH 11/51] fix: inject default label font as suggestion font --- src/main/kotlin/io/snyk/plugin/Utils.kt | 4 +++- .../snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 958eab791..4fe0c6dcb 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -263,7 +263,9 @@ fun refreshAnnotationsForOpenFiles(project: Project) { val psiFile = findPsiFileIgnoringExceptions(it, project) if (psiFile != null) { invokeLater { - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + if (!psiFile.project.isDisposed) { + DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 02bd0bf62..b20a9b985 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -4,6 +4,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import io.snyk.plugin.SnykFile import io.snyk.plugin.ui.DescriptionHeaderPanel import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -166,6 +167,8 @@ class SuggestionDescriptionPanelFromLS( val nonce = getNonce() html = html.replace("\${nonce}", nonce) + val fontUpdate = "--default-font: \"${UIUtil.getLabelFont().fontName}\", " + html = html.replace("--default-font:", fontUpdate) return html } From f5287aa5ba89d57c996f8b5947e3fffa8287f030 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 07:54:09 +0200 Subject: [PATCH 12/51] fix: shut up lsp4j logger during shutdown --- src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 1b557302b..a7fa1549f 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -42,6 +42,7 @@ 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.jsonrpc.json.StreamMessageProducer import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.LanguageServer import org.jetbrains.concurrency.runAsync @@ -66,6 +67,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.Level import java.util.logging.Logger.getLogger import kotlin.io.path.exists @@ -171,8 +173,11 @@ 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 messageProducerLogger = getLogger(StreamMessageProducer::class.java.name) + val previousMessageProducerLevel = messageProducerLogger.level val previousLSP4jLogLevel = lsp4jLogger.level - lsp4jLogger.level = java.util.logging.Level.OFF + lsp4jLogger.level = Level.OFF + messageProducerLogger.level = Level.OFF try { val shouldShutdown = lsIsAlive() executorService.submit { if (shouldShutdown) languageServer.shutdown().get(1, TimeUnit.SECONDS) } @@ -187,6 +192,7 @@ class LanguageServerWrapper( if (lsIsAlive()) process.destroyForcibly() } lsp4jLogger.level = previousLSP4jLogLevel + messageProducerLogger.level = previousMessageProducerLevel } } From e98d5eb05a318297184bc3b81d3e6cfaedb9acc5 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 09:21:29 +0200 Subject: [PATCH 13/51] feat: add issue vulnerability count / inline value support --- src/main/kotlin/io/snyk/plugin/Utils.kt | 2 + .../editor/LineEndingEditorFactoryListener.kt | 129 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 132 insertions(+) create mode 100644 src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index e6c316a02..958eab791 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -1,4 +1,5 @@ @file:JvmName("UtilsKt") +@file:Suppress("unused") package io.snyk.plugin @@ -427,6 +428,7 @@ fun Project.getContentRootPaths(): SortedSet { } fun Project.getContentRootVirtualFiles(): Set { + if (this.isDisposed) return emptySet() var contentRoots = ProjectRootManager.getInstance(this).contentRoots if (contentRoots.isEmpty()) { // this should cover for the case when no content roots are configured, e.g. in rider diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt new file mode 100644 index 000000000..02ed1e893 --- /dev/null +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -0,0 +1,129 @@ +package snyk.common.editor + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.LogLevel +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LineExtensionInfo +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.EditorFactoryListener +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiCompiledElement +import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.toLanguageServerURL +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.InlineValueContext +import org.eclipse.lsp4j.InlineValueParams +import org.eclipse.lsp4j.InlineValueText +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import org.eclipse.lsp4j.jsonrpc.messages.Either +import snyk.common.lsp.LanguageServerWrapper +import java.util.concurrent.TimeUnit + +class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + override fun dispose() { + disposed = true + } + + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + + override fun editorCreated(event: EditorFactoryEvent) { + if (disposed) return + val editor = event.editor + + if (editor.isOneLineMode) { + return + } + + val project = editor.project + if (project == null || project.isDisposed) { + return + } + + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) + if (psiFile == null || psiFile.virtualFile == null || psiFile is PsiCompiledElement) { + return + } + + if (editor !is EditorEx) { + return + } + + editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) + } + + class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) + + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } + + private var disposed = false + get() { + return ApplicationManager.getApplication().isDisposed || field + } + + fun isDisposed() = disposed + override fun dispose() { + disposed = true + } + + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + + fun getLineExtension(line: Int): MutableCollection { + if (disposed || !LanguageServerWrapper.getInstance() + .ensureLanguageServerInitialized() + ) return mutableSetOf() + + val document = editor.document + val lineEndOffset = document.getLineEndOffset(line) + val lineStartOffset = document.getLineStartOffset(line) + val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) + val ctx = InlineValueContext(0, range) + val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) + + val inlineValue = LanguageServerWrapper.getInstance().languageServer.textDocumentService.inlineValue(params) + @Suppress("USELESS_CAST") val text = try { + val result = inlineValue.get(120, TimeUnit.SECONDS) + if (result != null && result.isNotEmpty()) { + // this supposedly unneeded cast is needed as the conversion in lsp4j does not work correctly + val firstInlineValue = (result as List>)[0] + firstInlineValue.left.text + } else { + "" + } + } catch (ignored: Exception) { + // ignore error + "" + } + return mutableListOf(LineExtensionInfo("\t\t$text", attributes)) + } + + } +} + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d5e39c49b..e2c8a9936 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -56,6 +56,7 @@ + From 8d3be70cd35ffb7d96f3e26f5ab909b4ecefced3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 09:22:12 +0200 Subject: [PATCH 14/51] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0640478e2..a84c21128 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - always display info nodes - add option in IntelliJ registry to display tooltips with issue information - display documentation info when hovering over issue +- add inline value support for display of vulnerability count ### Fixes - add name to code vision provider From 5b72423463af1bd7f4a1db349226c30ee0cf2e5f Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:11:06 +0200 Subject: [PATCH 15/51] fix: early exit editor painting if no issues known --- .../editor/LineEndingEditorFactoryListener.kt | 46 +++++++++++-------- .../snyk/common/lsp/LanguageServerWrapper.kt | 2 +- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 02ed1e893..9b7aa234c 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import org.eclipse.lsp4j.InlineValueContext @@ -69,15 +70,12 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) } - class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { - private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT - val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) - val logger = Logger.getInstance(this::class.java).apply { - // tie log level to language server log level - val languageServerWrapper = LanguageServerWrapper.getInstance() - if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) - if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + companion object { + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) + val ctx = InlineValueContext(0, Range()) } private var disposed = false @@ -86,6 +84,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { } fun isDisposed() = disposed + override fun dispose() { disposed = true } @@ -94,25 +93,36 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { Disposer.register(SnykPluginDisposable.getInstance(), this) } + val project = editor.project + val document = editor.document + val snykCachedResults = project?.let { getSnykCachedResults(it) } + val languageServerWrapper = LanguageServerWrapper.getInstance() + val logger = Logger.getInstance(this::class.java).apply { + // tie log level to language server log level + if (languageServerWrapper.logger.isDebugEnabled) this.setLevel(LogLevel.DEBUG) + if (languageServerWrapper.logger.isTraceEnabled) this.setLevel(LogLevel.TRACE) + } + fun getLineExtension(line: Int): MutableCollection { - if (disposed || !LanguageServerWrapper.getInstance() - .ensureLanguageServerInitialized() + // only OSS has inline values right now + val hasResults = snykCachedResults?.currentOSSResultsLS?.isNotEmpty() ?: false + if (disposed + || !languageServerWrapper.isInitialized + || !hasResults ) return mutableSetOf() - val document = editor.document val lineEndOffset = document.getLineEndOffset(line) val lineStartOffset = document.getLineStartOffset(line) val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) - val ctx = InlineValueContext(0, range) val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) - - val inlineValue = LanguageServerWrapper.getInstance().languageServer.textDocumentService.inlineValue(params) - @Suppress("USELESS_CAST") val text = try { - val result = inlineValue.get(120, TimeUnit.SECONDS) + val inlineValue = languageServerWrapper.languageServer.textDocumentService.inlineValue(params) + val text = try { + val result = inlineValue.get(5, TimeUnit.MILLISECONDS) if (result != null && result.isNotEmpty()) { // this supposedly unneeded cast is needed as the conversion in lsp4j does not work correctly - val firstInlineValue = (result as List>)[0] - firstInlineValue.left.text + val eitherList = result as List> + val firstInlineValue = eitherList[0] + firstInlineValue.left.text ?: "" } else { "" } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index a7fa1549f..1b541e0e7 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -370,7 +370,7 @@ class LanguageServerWrapper( val param = ExecuteCommandParams() param.command = COMMAND_GET_FEATURE_FLAG_STATUS param.arguments = listOf(featureFlag) - val result = languageServer.workspaceService.executeCommand(param).get(10, TimeUnit.SECONDS) + val result = languageServer.workspaceService.executeCommand(param).get(20, TimeUnit.SECONDS) val resultMap = result as? Map<*, *> val ok = resultMap?.get("ok") as? Boolean ?: false From 2e7bd27499d52109187a6fe5587c109b847d5625 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:13:01 +0200 Subject: [PATCH 16/51] fix: catch timeout exception from documentation provider --- .../common/lsp/LSDocumentationTargetProvider.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt index 66fb4ae47..d19f12cb9 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt @@ -19,6 +19,7 @@ import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.TextDocumentIdentifier import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException class SnykDocumentationTargetPointer(private val documentationTarget: DocumentationTarget) : Pointer { @@ -46,10 +47,14 @@ class LSDocumentationTargetProvider : DocumentationTargetProvider, Disposable { 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)) + try { + val hover = + languageServerWrapper.languageServer.textDocumentService.hover(hoverParams).get(5, TimeUnit.SECONDS) + if (hover == null || hover.contents.right.value.isEmpty()) return mutableListOf() + return mutableListOf(SnykDocumentationTarget(hover)) + } catch (ignored: TimeoutException) { + return mutableListOf() + } } inner class SnykDocumentationTarget(private val hover: Hover) : DocumentationTarget { From cb967a74f7990791a04e8161791349286d9ccb89 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:25:46 +0200 Subject: [PATCH 17/51] fix: check cache on file level --- .../editor/LineEndingEditorFactoryListener.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 9b7aa234c..4b73b40bd 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -14,9 +14,9 @@ import com.intellij.openapi.editor.event.EditorFactoryListener import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.PsiCompiledElement import com.intellij.psi.PsiDocumentManager +import io.snyk.plugin.SnykFile import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable @@ -59,7 +59,8 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { } val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) - if (psiFile == null || psiFile.virtualFile == null || psiFile is PsiCompiledElement) { + val virtualFile = psiFile?.virtualFile + if (psiFile == null || virtualFile == null || psiFile is PsiCompiledElement) { return } @@ -67,13 +68,15 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { return } - editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, psiFile.virtualFile)::getLineExtension) + val snykFile = SnykFile(project, virtualFile) + + editor.registerLineExtensionPainter(EditorLineEndingProvider(editor, snykFile)::getLineExtension) } - class EditorLineEndingProvider(val editor: Editor, val virtualFile: VirtualFile) : Disposable { + class EditorLineEndingProvider(val editor: Editor, val snykFile: SnykFile) : Disposable { companion object { - private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.LINE_COMMENT + private val attributeKey: TextAttributesKey = DefaultLanguageHighlighterColors.INLAY_TEXT_WITHOUT_BACKGROUND val attributes: TextAttributes = EditorColorsManager.getInstance().globalScheme.getAttributes(attributeKey) val ctx = InlineValueContext(0, Range()) } @@ -105,7 +108,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { fun getLineExtension(line: Int): MutableCollection { // only OSS has inline values right now - val hasResults = snykCachedResults?.currentOSSResultsLS?.isNotEmpty() ?: false + val hasResults = snykCachedResults?.currentOSSResultsLS?.get(snykFile)?.isNotEmpty() ?: false if (disposed || !languageServerWrapper.isInitialized || !hasResults @@ -114,7 +117,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { val lineEndOffset = document.getLineEndOffset(line) val lineStartOffset = document.getLineStartOffset(line) val range = Range(Position(line, 0), Position(line, lineEndOffset - lineStartOffset)) - val params = InlineValueParams(TextDocumentIdentifier(virtualFile.toLanguageServerURL()), range, ctx) + val params = InlineValueParams(TextDocumentIdentifier(snykFile.virtualFile.toLanguageServerURL()), range, ctx) val inlineValue = languageServerWrapper.languageServer.textDocumentService.inlineValue(params) val text = try { val result = inlineValue.get(5, TimeUnit.MILLISECONDS) From ae30c1e042306a7ade91a456e7b5f04bb3caec29 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:29:23 +0200 Subject: [PATCH 18/51] fix: remove second tab from inline value display --- .../snyk/common/editor/LineEndingEditorFactoryListener.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 4b73b40bd..0d72f2101 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -133,7 +133,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { // ignore error "" } - return mutableListOf(LineExtensionInfo("\t\t$text", attributes)) + return mutableListOf(LineExtensionInfo("\t$text", attributes)) } } From 3a19b768bb16dbc5af02866cada076d4f9b696fa Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:42:44 +0200 Subject: [PATCH 19/51] fix: don't scan double on branch choosing --- src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt index 547bc2401..042d21376 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -68,7 +68,6 @@ class BranchChooserComboBoxDialog(val project: Project) : DialogWrapper(true) { } runAsync { LanguageServerWrapper.getInstance().updateConfiguration() - LanguageServerWrapper.getInstance().sendScanCommand(project) } } From 7d6a05a6c19c47869d97f37184b68e64aff88299 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 10:45:56 +0200 Subject: [PATCH 20/51] fix: refresh feature flags once before scanning --- src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 1b541e0e7..e10a3a666 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -189,7 +189,7 @@ class LanguageServerWrapper( } catch (ignore: Exception) { // do nothing } finally { - if (lsIsAlive()) process.destroyForcibly() + if (lsIsAlive()) process.destroyForcibly() } lsp4jLogger.level = previousLSP4jLogLevel messageProducerLogger.level = previousMessageProducerLevel @@ -343,8 +343,8 @@ class LanguageServerWrapper( fun sendScanCommand(project: Project) { if (!ensureLanguageServerInitialized()) return DumbService.getInstance(project).runWhenSmart { + refreshFeatureFlags() getTrustedContentRoots(project).forEach { - refreshFeatureFlags() sendFolderScanCommand(it.path, project) } } From 0c606629fb99250cbbe435d9318fde2387f1ac1c Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 11:29:38 +0200 Subject: [PATCH 21/51] fix: flickering when rendering html --- .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 24 ++++++++++++++----- .../snyk/common/lsp/LanguageServerWrapper.kt | 1 + 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt index d5998e20d..273027b5e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -3,12 +3,15 @@ package io.snyk.plugin.ui.jcef import com.intellij.ui.jcef.JBCefApp import com.intellij.ui.jcef.JBCefBrowser import com.intellij.ui.jcef.JBCefBrowserBuilder +import com.intellij.ui.jcef.JBCefClient import org.cef.handler.CefLoadHandlerAdapter import java.awt.Component typealias LoadHandlerGenerator = (jbCefBrowser: JBCefBrowser) -> CefLoadHandlerAdapter object JCEFUtils { + private val jbCefPair : Pair? = null + fun getJBCefBrowserComponentIfSupported( html: String, loadHandlerGenerators: List, @@ -16,12 +19,7 @@ object JCEFUtils { if (!JBCefApp.isSupported()) { return null } - val cefClient = JBCefApp.getInstance().createClient() - cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) - val jbCefBrowser = - JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) - .setMouseWheelEventEnable(true).build() - jbCefBrowser.setOpenLinksInExternalBrowser(true) + val (cefClient, jbCefBrowser) = getBrowser() for (loadHandlerGenerator in loadHandlerGenerators) { val loadHandler = loadHandlerGenerator(jbCefBrowser) @@ -31,4 +29,18 @@ object JCEFUtils { return jbCefBrowser.component } + + private fun getBrowser(): Pair { + if (jbCefPair != null) { + return jbCefPair + } + val cefClient = JBCefApp.getInstance().createClient() + cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) + val jbCefBrowser = + JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) + .setMouseWheelEventEnable(true).build() + jbCefBrowser.setOpenLinksInExternalBrowser(true) + val jbCefPair = Pair(cefClient, jbCefBrowser) + return jbCefPair + } } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index e10a3a666..21f8779b7 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -402,6 +402,7 @@ class LanguageServerWrapper( languageServer.workspaceService.executeCommand(param) } catch (ignored: Exception) { // do nothing to not break UX for analytics + // TODO review } } From 1101b35f39ecdd4c524e0f69f64e1473d8e59901 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Sat, 14 Sep 2024 13:02:50 +0200 Subject: [PATCH 22/51] fix: inject default label font as suggestion font --- src/main/kotlin/io/snyk/plugin/Utils.kt | 4 +++- .../snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 958eab791..4fe0c6dcb 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -263,7 +263,9 @@ fun refreshAnnotationsForOpenFiles(project: Project) { val psiFile = findPsiFileIgnoringExceptions(it, project) if (psiFile != null) { invokeLater { - DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + if (!psiFile.project.isDisposed) { + DaemonCodeAnalyzer.getInstance(project).restart(psiFile) + } } } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 95c1751d0..9cc1dfdc9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil import io.snyk.plugin.SnykFile import io.snyk.plugin.ui.DescriptionHeaderPanel import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -170,6 +171,8 @@ class SuggestionDescriptionPanelFromLS( val nonce = getNonce() html = html.replace("\${nonce}", nonce) + val fontUpdate = "--default-font: \"${UIUtil.getLabelFont().fontName}\", " + html = html.replace("--default-font:", fontUpdate) return html } From 96e6af23376be48092d72f57ae255657e292a381 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 18 Sep 2024 09:16:23 +0300 Subject: [PATCH 23/51] chore: bump protocol version --- .../snyk/plugin/services/SnykApplicationSettingsStateService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index b2db93677..24c55bb6d 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -26,7 +26,7 @@ import java.util.UUID storages = [Storage("snyk.settings.xml", roamingType = RoamingType.DISABLED)], ) class SnykApplicationSettingsStateService : PersistentStateComponent { - val requiredLsProtocolVersion = 14 + val requiredLsProtocolVersion = 15 var useTokenAuthentication = false var currentLSProtocolVersion: Int? = 0 From 64744a9a42066903ee334d09f0b8c464e2143ae7 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 20 Sep 2024 11:49:27 +0200 Subject: [PATCH 24/51] chore: wip --- src/main/kotlin/io/snyk/plugin/SnykFile.kt | 3 +- .../io/snyk/plugin/SnykPostStartupActivity.kt | 3 - src/main/kotlin/io/snyk/plugin/Utils.kt | 8 +- .../snyk/plugin/events/SnykScanListenerLS.kt | 2 + .../plugin/services/SnykTaskQueueService.kt | 6 - .../plugin/ui/SnykBalloonNotifications.kt | 3 +- .../plugin/ui/toolwindow/SnykToolWindow.kt | 4 + .../ui/toolwindow/SnykToolWindowPanel.kt | 1 + .../SnykToolWindowSnykScanListenerLS.kt | 35 ++- .../ui/toolwindow/SnykTreeCellRenderer.kt | 64 +---- .../panels/IssueDescriptionPanelBase.kt | 2 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 24 +- .../kotlin/snyk/common/SnykCachedResults.kt | 4 +- .../common/annotator/CodeActionIntention.kt | 2 +- .../LSCodeVisionProvider.kt | 18 +- .../common/{lsp => editor}/DocumentChanger.kt | 2 +- .../snyk/common/lsp/LanguageServerWrapper.kt | 3 +- .../snyk/common/lsp/SnykLanguageClient.kt | 7 +- src/main/kotlin/snyk/common/lsp/Types.kt | 241 +++++------------- src/main/kotlin/snyk/trust/TrustedProjects.kt | 3 +- src/main/resources/META-INF/plugin.xml | 6 +- .../SnykToolWindowSnykScanListenerLSTest.kt | 5 + 22 files changed, 158 insertions(+), 288 deletions(-) rename src/main/kotlin/snyk/common/{lsp => codevision}/LSCodeVisionProvider.kt (87%) rename src/main/kotlin/snyk/common/{lsp => editor}/DocumentChanger.kt (98%) diff --git a/src/main/kotlin/io/snyk/plugin/SnykFile.kt b/src/main/kotlin/io/snyk/plugin/SnykFile.kt index 826a0aa3a..35ed415e5 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykFile.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykFile.kt @@ -3,11 +3,12 @@ package io.snyk.plugin import com.intellij.openapi.project.Project import com.intellij.openapi.util.Iconable import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.concurrency.runAsync import snyk.common.RelativePathHelper import javax.swing.Icon data class SnykFile(val project: Project, val virtualFile: VirtualFile) { - val relativePath = RelativePathHelper().getRelativePath(virtualFile, project) + val relativePath = runAsync { RelativePathHelper().getRelativePath(virtualFile, project) } } fun toSnykFileSet(project: Project, virtualFiles: Set) = diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt index 1805c834a..810904ec5 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt @@ -5,7 +5,6 @@ import com.intellij.ide.plugins.PluginInstaller import com.intellij.ide.plugins.PluginStateListener import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager @@ -23,8 +22,6 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.Date -private val LOG = logger() - private const val EXTENSION_POINT_CONTROLLER_MANAGER = "io.snyk.snyk-intellij-plugin.controllerManager" class SnykPostStartupActivity : ProjectActivity { diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 4fe0c6dcb..c7cf7793c 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -172,6 +172,11 @@ fun cancelOssIndicator(project: Project) { indicator?.cancel() } +fun cancelIacIndicator(project: Project) { + val indicator = getSnykTaskQueueService(project)?.iacScanProgressIndicator + indicator?.cancel() +} + fun isSnykCodeRunning(project: Project): Boolean { val lsRunning = project.getContentRootVirtualFiles().any { vf -> val key = ScanInProgressKey(vf, ProductType.CODE_SECURITY) @@ -216,9 +221,6 @@ fun controlExternalProcessWithProgressIndicator( fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled -fun isSnykIaCLSEnabled(): Boolean = false - - fun isDocumentationHoverEnabled(): Boolean = Registry.get("snyk.isDocumentationHoverEnabled").asBoolean() fun getWaitForResultsTimeout(): Long = diff --git a/src/main/kotlin/io/snyk/plugin/events/SnykScanListenerLS.kt b/src/main/kotlin/io/snyk/plugin/events/SnykScanListenerLS.kt index 3925eb717..cdd292619 100644 --- a/src/main/kotlin/io/snyk/plugin/events/SnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/events/SnykScanListenerLS.kt @@ -17,6 +17,8 @@ interface SnykScanListenerLS { fun scanningOssFinished() + fun scanningIacFinished() + fun scanningError(snykScan: SnykScanParams) fun onPublishDiagnostics(product: String, snykFile: SnykFile, issueList: List) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index b23b9c75f..807ee6839 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -23,7 +23,6 @@ import io.snyk.plugin.isCliDownloading import io.snyk.plugin.isCliInstalled import io.snyk.plugin.isOssRunning import io.snyk.plugin.isSnykCodeRunning -import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.ui.SnykBalloonNotificationHelper @@ -103,11 +102,6 @@ class SnykTaskQueueService(val project: Project) { LanguageServerWrapper.getInstance().sendScanCommand(project) - if (settings.iacScanEnabled) { - if (!isSnykIaCLSEnabled()) { - scheduleIacScan() - } - } if (settings.containerScanEnabled) { scheduleContainerScan() } diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotifications.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotifications.kt index 9e98ab306..e3f9abd66 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotifications.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykBalloonNotifications.kt @@ -12,7 +12,8 @@ import io.snyk.plugin.snykToolWindow object SnykBalloonNotifications { private val logger = logger() - private val notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Snyk") + private val notificationGroup + get() = NotificationGroupManager.getInstance().getNotificationGroup("Snyk") fun showWelcomeNotification(project: Project) { val welcomeMessage = "Welcome to Snyk! Check out our tool window to start analyzing your code" diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt index 0b346ad39..f9ab24016 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt @@ -91,6 +91,10 @@ class SnykToolWindow(private val project: Project) : SimpleToolWindowPanel(false updateActionsPresentation() } + override fun scanningIacFinished() { + updateActionsPresentation() + } + override fun scanningError(snykScan: SnykScanParams) { updateActionsPresentation() } 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 0c7bd392d..44ff98fd9 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -155,6 +155,7 @@ class SnykToolWindowPanel( rootSecurityIssuesTreeNode, rootQualityIssuesTreeNode, rootOssTreeNode, + rootIacIssuesTreeNode ) project.messageBus.connect(this).subscribe( SnykScanListenerLS.SNYK_SCAN_TOPIC, 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 ae02db22e..7028da6bb 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -10,6 +10,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.ui.tree.TreeUtil import io.snyk.plugin.Severity import io.snyk.plugin.SnykFile +import io.snyk.plugin.cancelIacIndicator import io.snyk.plugin.cancelOssIndicator import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getSnykCachedResults @@ -17,6 +18,7 @@ import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_QUALITY_ROOT_TEXT import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_ROOT_TEXT +import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.IAC_ROOT_TEXT 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 @@ -45,6 +47,7 @@ class SnykToolWindowSnykScanListenerLS( private val rootSecurityIssuesTreeNode: DefaultMutableTreeNode, private val rootQualityIssuesTreeNode: DefaultMutableTreeNode, private val rootOssIssuesTreeNode: DefaultMutableTreeNode, + private val rootIacIssuesTreeNode: DefaultMutableTreeNode, ) : SnykScanListenerLS, Disposable { private var disposed = false get() { @@ -95,6 +98,19 @@ class SnykToolWindowSnykScanListenerLS( refreshAnnotationsForOpenFiles(project) } + override fun scanningIacFinished() { + if (disposed) return + cancelIacIndicator(project) + ApplicationManager.getApplication().invokeLater { + this.rootIacIssuesTreeNode.userObject = "$IAC_ROOT_TEXT (scanning finished)" + this.snykToolWindowPanel.triggerSelectionListeners = false + val snykCachedResults = getSnykCachedResults(project) + displayIacResults(snykCachedResults?.currentIacResultsLS ?: emptyMap()) + this.snykToolWindowPanel.triggerSelectionListeners = true + } + refreshAnnotationsForOpenFiles(project) + } + override fun scanningError(snykScan: SnykScanParams) { when (snykScan.product) { "oss" -> { @@ -110,7 +126,8 @@ class SnykToolWindowSnykScanListenerLS( } "iac" -> { - // TODO implement + this.rootIacIssuesTreeNode.removeAllChildren() + this.rootOssIssuesTreeNode.userObject = "$IAC_ROOT_TEXT (error)" } "container" -> { @@ -175,6 +192,22 @@ class SnykToolWindowSnykScanListenerLS( ) } + fun displayIacResults(snykResults: Map>) { + if (disposed) return + if (getSnykCachedResults(project)?.currentIacError != null) return + + val settings = pluginSettings() + + displayIssues( + enabledInSettings = settings.iacScanEnabled, + filterTree = settings.treeFiltering.iacResults, + snykResults = snykResults, + rootNode = this.rootIacIssuesTreeNode, + iacResultsCount = snykResults.values.flatten().distinct().size, + fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable } + ) + } + private fun displayIssues( enabledInSettings: Boolean, filterTree: Boolean, 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 05dd3acd9..458d0f3a0 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -88,39 +88,6 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { } } - is IacFileTreeNode -> { - val iacVulnerabilitiesForFile = value.userObject as IacIssuesForFile - nodeIcon = - getIcon( - iacVulnerabilitiesForFile.packageManager.lowercase(Locale.getDefault()), - ) - val relativePath = - iacVulnerabilitiesForFile.relativePath ?: iacVulnerabilitiesForFile.targetFilePath - toolTipText = - buildString { - append(relativePath) - append(ProductType.IAC.getCountText(value.childCount)) - } - - text = - toolTipText.apply { - if (toolTipText.length > MAX_FILE_TREE_NODE_LENGTH) { - "..." + - this.substring( - this.length - MAX_FILE_TREE_NODE_LENGTH, - this.length, - ) - } - } - - val snykCachedResults = getSnykCachedResults(value.project) - if (snykCachedResults?.currentIacResult == null || iacVulnerabilitiesForFile.obsolete) { - attributes = SimpleTextAttributes.GRAYED_ATTRIBUTES - nodeIcon = getDisabledIcon(nodeIcon) - text += OBSOLETE_SUFFIX - } - } - is ContainerImageTreeNode -> { val issuesForImage = value.userObject as ContainerIssuesForImage nodeIcon = SnykIcons.CONTAINER_IMAGE @@ -150,23 +117,6 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { text = info } - is IacIssueTreeNode -> { - val issue = (value.userObject as IacIssue) - val snykCachedResults = getSnykCachedResults(value.project) - nodeIcon = SnykIcons.getSeverityIcon(issue.getSeverity()) - val prefix = if (issue.lineNumber > 0) "line ${issue.lineNumber}: " else "" - text = prefix + issue.title + - when { - issue.ignored -> IGNORED_SUFFIX - snykCachedResults?.currentIacResult == null || issue.obsolete -> OBSOLETE_SUFFIX - else -> "" - } - if (snykCachedResults?.currentIacResult == null || issue.ignored || issue.obsolete) { - attributes = SimpleTextAttributes.GRAYED_ATTRIBUTES - nodeIcon = getDisabledIcon(nodeIcon) - } - } - is ContainerIssueTreeNode -> { val issue = value.userObject as ContainerIssue val snykCachedResults = getSnykCachedResults(value.project) @@ -283,12 +233,14 @@ class SnykTreeCellRenderer : ColoredTreeCellRenderer() { value: DefaultMutableTreeNode, firstIssue: ScanIssue?, ): Pair { - val relativePath = file.relativePath - toolTipText = - buildString { - append(relativePath) - append(productType.getCountText(value.childCount)) - } + + file.relativePath.then { + toolTipText = + buildString { + append(it) + append(productType.getCountText(value.childCount)) + } + } val text = toolTipText.apply { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt index 7927277ab..6cad0109d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt @@ -50,7 +50,7 @@ abstract class IssueDescriptionPanelBase( add(mainBodyPanel, BorderLayout.CENTER) } - fun titlePanel(insets: Insets = Insets(20, 10, 20, 20), indent: Int = 1): JPanel { + fun titlePanel(insets: Insets = JBUI.insets(20, 10, 20, 20), indent: Int = 1): JPanel { val titlePanel = JPanel() titlePanel.layout = GridLayoutManager(2, 2, insets, -1, 5) val titleLabel = JLabel().apply { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 9cc1dfdc9..cbf71b59d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -20,6 +20,7 @@ import io.snyk.plugin.ui.panelGridConstraints import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel import io.snyk.plugin.ui.wrapWithScrollPane import snyk.common.ProductType +import snyk.common.lsp.FilterableIssueType import snyk.common.lsp.ScanIssue import stylesheets.SnykStylesheets import java.awt.BorderLayout @@ -49,8 +50,8 @@ class SuggestionDescriptionPanelFromLS( ThemeBasedStylingGenerator().generate(it) } - if (issue.additionalData.getProductType() == ProductType.CODE_SECURITY || - issue.additionalData.getProductType() == ProductType.CODE_QUALITY + if (issue.filterableIssueType == FilterableIssueType.CodeQuality || + issue.filterableIssueType == FilterableIssueType.CodeSecurity ) { val virtualFiles = LinkedHashMap() for (dataFlow in issue.additionalData.dataFlow) { @@ -118,8 +119,8 @@ class SuggestionDescriptionPanelFromLS( JPanel( GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20), ).apply { - if (issue.additionalData.getProductType() == ProductType.CODE_SECURITY || - issue.additionalData.getProductType() == ProductType.CODE_QUALITY + if (issue.filterableIssueType == FilterableIssueType.CodeSecurity || + issue.filterableIssueType == FilterableIssueType.CodeQuality ) { this.add( SnykCodeOverviewPanel(issue.additionalData), @@ -133,7 +134,7 @@ class SuggestionDescriptionPanelFromLS( SnykCodeExampleFixesPanel(issue.additionalData), panelGridConstraints(4), ) - } else if (issue.additionalData.getProductType() == ProductType.OSS) { + } else if (issue.filterableIssueType == FilterableIssueType.OpenSource) { this.add( SnykOSSIntroducedThroughPanel(issue.additionalData), baseGridConstraintsAnchorWest(1, indent = 0), @@ -147,7 +148,7 @@ class SuggestionDescriptionPanelFromLS( panelGridConstraints(3), ) } else { - TODO() + // do nothing } } return Pair(panel, lastRowToAddSpacer) @@ -156,13 +157,12 @@ class SuggestionDescriptionPanelFromLS( fun getCustomCssAndScript(): String { var html = issue.details() val ideScript = getCustomScript() - var ideStyle = "" - if (issue.additionalData.getProductType() == ProductType.CODE_SECURITY || - issue.additionalData.getProductType() == ProductType.CODE_QUALITY + val ideStyle: String = if (issue.filterableIssueType == FilterableIssueType.CodeSecurity || + issue.filterableIssueType == FilterableIssueType.CodeQuality ) { - ideStyle = SnykStylesheets.SnykCodeSuggestion - } else if (issue.additionalData.getProductType() == ProductType.OSS) { - ideStyle = SnykStylesheets.SnykOSSSuggestion + SnykStylesheets.SnykCodeSuggestion + } else { + SnykStylesheets.SnykOSSSuggestion } html = html.replace("\${ideStyle}", "") diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 3f1ceb591..6ce2b688b 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -125,8 +125,8 @@ class SnykCachedResults( } override fun scanningSnykCodeFinished() = Unit - override fun scanningOssFinished() = Unit + override fun scanningIacFinished() = Unit override fun scanningError(snykScan: SnykScanParams) { when (snykScan.product) { @@ -196,7 +196,7 @@ class SnykCachedResults( } LsProductConstants.InfrastructureAsCode.value -> { - + currentIacResultsLS[snykFile] = issueList } LsProductConstants.Container.value -> { diff --git a/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt index e230e8155..6b47317e8 100644 --- a/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt +++ b/src/main/kotlin/snyk/common/annotator/CodeActionIntention.kt @@ -14,7 +14,7 @@ import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.TextEdit import snyk.common.ProductType import snyk.common.intentionactions.SnykIntentionActionBase -import snyk.common.lsp.DocumentChanger +import snyk.common.editor.DocumentChanger import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import java.util.concurrent.TimeUnit diff --git a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt similarity index 87% rename from src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt rename to src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt index 610ccc4fa..1237768a1 100644 --- a/src/main/kotlin/snyk/common/lsp/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt @@ -1,10 +1,6 @@ -package snyk.common.lsp +package snyk.common.codevision -import com.intellij.codeInsight.codeVision.CodeVisionAnchorKind -import com.intellij.codeInsight.codeVision.CodeVisionEntry -import com.intellij.codeInsight.codeVision.CodeVisionProvider -import com.intellij.codeInsight.codeVision.CodeVisionRelativeOrdering -import com.intellij.codeInsight.codeVision.CodeVisionState +import com.intellij.codeInsight.codeVision.* import com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider import com.intellij.codeInsight.codeVision.ui.model.ClickableTextCodeVisionEntry import com.intellij.openapi.application.ReadAction @@ -12,7 +8,7 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.Editor import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager @@ -23,15 +19,15 @@ import org.eclipse.lsp4j.CodeLens import org.eclipse.lsp4j.CodeLensParams import org.eclipse.lsp4j.ExecuteCommandParams import org.eclipse.lsp4j.TextDocumentIdentifier +import snyk.common.lsp.LanguageServerWrapper import java.awt.event.MouseEvent import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException -private const val CODELENS_FETCH_TIMEOUT = 10L - @Suppress("UnstableApiUsage") class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingProvider { private val logger = logger() + private val timeout = 10L override val defaultAnchor: CodeVisionAnchorKind = CodeVisionAnchorKind.Default override val id = "snyk.common.lsp.LSCodeVisionProvider" override val name = "Snyk Security Language Server Code Vision Provider" @@ -62,7 +58,7 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro val lenses = mutableListOf>() val codeLenses = try { LanguageServerWrapper.getInstance().languageServer.textDocumentService.codeLens(params) - .get(CODELENS_FETCH_TIMEOUT, TimeUnit.SECONDS) ?: return CodeVisionState.READY_EMPTY + .get(timeout, TimeUnit.SECONDS) ?: return CodeVisionState.READY_EMPTY } catch (ignored: TimeoutException) { logger.info("Timeout fetching code lenses for : $file") return CodeVisionState.READY_EMPTY @@ -91,7 +87,7 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro override fun invoke(event: MouseEvent?, editor: Editor) { event ?: return - val task = object : Backgroundable(editor.project, "Executing ${codeLens.command.title}", false) { + val task = object : Task.Backgroundable(editor.project, "Executing ${codeLens.command.title}", false) { override fun run(indicator: ProgressIndicator) { val params = ExecuteCommandParams(codeLens.command.command, codeLens.command.arguments) try { diff --git a/src/main/kotlin/snyk/common/lsp/DocumentChanger.kt b/src/main/kotlin/snyk/common/editor/DocumentChanger.kt similarity index 98% rename from src/main/kotlin/snyk/common/lsp/DocumentChanger.kt rename to src/main/kotlin/snyk/common/editor/DocumentChanger.kt index 7015747cf..baa40d4a0 100644 --- a/src/main/kotlin/snyk/common/lsp/DocumentChanger.kt +++ b/src/main/kotlin/snyk/common/editor/DocumentChanger.kt @@ -1,4 +1,4 @@ -package snyk.common.lsp +package snyk.common.editor import com.intellij.openapi.vfs.VirtualFileManager import io.snyk.plugin.getDocument diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 21f8779b7..9e480358c 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -15,7 +15,6 @@ import io.snyk.plugin.getCliFile import io.snyk.plugin.getContentRootVirtualFiles import io.snyk.plugin.getSnykTaskQueueService import io.snyk.plugin.getWaitForResultsTimeout -import io.snyk.plugin.isSnykIaCLSEnabled import io.snyk.plugin.pluginSettings import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable @@ -418,7 +417,7 @@ class LanguageServerWrapper( activateSnykOpenSource = ps.ossScanEnable.toString(), activateSnykCodeSecurity = ps.snykCodeSecurityIssuesScanEnable.toString(), activateSnykCodeQuality = ps.snykCodeQualityIssuesScanEnable.toString(), - activateSnykIac = isSnykIaCLSEnabled().toString(), + activateSnykIac = ps.iacScanEnabled.toString(), organization = ps.organization, insecure = ps.ignoreUnknownCA.toString(), endpoint = getEndpointUrl(), diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index b6c99ba97..59202c8fa 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -52,6 +52,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.services.LanguageClient import org.jetbrains.concurrency.runAsync import snyk.common.ProductType +import snyk.common.editor.DocumentChanger import snyk.trust.WorkspaceTrustService import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CompletableFuture @@ -122,6 +123,7 @@ class SnykLanguageClient : if (firstDiagnostic == null) { scanPublisher.onPublishDiagnostics("code", snykFile, emptyList()) scanPublisher.onPublishDiagnostics("oss", snykFile, emptyList()) + scanPublisher.onPublishDiagnostics("iac", snykFile, emptyList()) return } @@ -144,7 +146,7 @@ class SnykLanguageClient : } LsProductConstants.InfrastructureAsCode.value -> { - // TODO implement + scanPublisher.onPublishDiagnostics(product, snykFile, issueList) } LsProductConstants.Container.value -> { @@ -227,6 +229,7 @@ class SnykLanguageClient : when (snykScan.product) { "code" -> ProductType.CODE_SECURITY "oss" -> ProductType.OSS + "iac" -> ProductType.IAC else -> return } val key = ScanInProgressKey(snykScan.folderPath.toVirtualFile(), product) @@ -266,7 +269,7 @@ class SnykLanguageClient : } "iac" -> { - // TODO implement + scanPublisher.scanningIacFinished() } "container" -> { diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 67773efa6..3ced9b917 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -1,4 +1,4 @@ -@file:Suppress("unused", "SENSELESS_COMPARISON", "UNNECESSARY_SAFE_CALL", "USELESS_ELVIS", "DuplicatedCode") +@file:Suppress("unused", "DuplicatedCode") package snyk.common.lsp @@ -57,6 +57,14 @@ enum class LsProductConstants(val value: String) { } } +enum class FilterableIssueType(val value: String) { + OpenSource("Open Source"), + CodeQuality("Code Quality"), + CodeSecurity("Code Security"), + InfrastructureAsCode("Infrastructure As Code"), + Container("Container") +} + // Define the ScanIssue data class data class ScanIssue( @@ -67,6 +75,8 @@ data class ScanIssue( val range: Range, val additionalData: IssueData, var isIgnored: Boolean?, + var isNew: Boolean?, + var filterableIssueType: FilterableIssueType, var ignoreDetails: IgnoreDetails?, ) : Comparable { var textRange: TextRange? = null @@ -126,13 +136,13 @@ data class ScanIssue( } fun title(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.title - ProductType.CODE_QUALITY -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> this.title + FilterableIssueType.CodeQuality -> { this.additionalData.message.split('.').firstOrNull() ?: "Unknown issue" } - ProductType.CODE_SECURITY -> { + FilterableIssueType.CodeSecurity -> { this.title.split(":").firstOrNull() ?: "Unknown issue" } @@ -141,12 +151,12 @@ data class ScanIssue( } fun longTitle(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> { "${this.additionalData.packageName}@${this.additionalData.version}: ${this.title()}" } - ProductType.CODE_QUALITY, ProductType.CODE_SECURITY -> { + FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity, FilterableIssueType.InfrastructureAsCode -> { return "${this.title()} [${this.range.start.line + 1},${this.range.start.character}]" } @@ -155,8 +165,8 @@ data class ScanIssue( } fun priority(): Int { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> { return when (this.getSeverityAsEnum()) { Severity.CRITICAL -> 4 Severity.HIGH -> 3 @@ -166,34 +176,35 @@ data class ScanIssue( } } - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> this.additionalData.priorityScore + FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity -> this.additionalData.priorityScore else -> TODO() } } fun issueNaming(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> { if (this.additionalData.license != null) { "License" } else { - "Vulnerability" + "Issue" } } - ProductType.CODE_SECURITY -> "Vulnerability" - ProductType.CODE_QUALITY -> "Quality Issue" + FilterableIssueType.CodeSecurity -> "Security Issue" + FilterableIssueType.CodeQuality -> "Quality Issue" + FilterableIssueType.InfrastructureAsCode -> "Configuration Issue" else -> TODO() } } fun cwes(): List { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> { this.additionalData.identifiers?.CWE ?: emptyList() } - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> { + FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity -> { this.additionalData.cwe ?: emptyList() } @@ -202,101 +213,75 @@ data class ScanIssue( } fun cves(): List { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> { + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> { this.additionalData.identifiers?.CVE ?: emptyList() } - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> emptyList() + FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity, FilterableIssueType.InfrastructureAsCode -> emptyList() else -> TODO() } } fun cvssScore(): String? { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.additionalData.cvssScore - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> null - else -> TODO() + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> this.additionalData.cvssScore + else -> null } } fun cvssV3(): String? { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.additionalData.CVSSv3 - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> null - else -> TODO() + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> this.additionalData.CVSSv3 + else -> null } } fun id(): String? { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.additionalData.ruleId - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> null - else -> TODO() + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> this.additionalData.ruleId + else -> null } } fun ruleId(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS, ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> this.additionalData.ruleId - else -> TODO() - } + return this.additionalData.ruleId } fun icon(): Icon? { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> getIcon(this.additionalData.packageManager.lowercase(Locale.getDefault())) - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> null - else -> TODO() + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> getIcon(this.additionalData.packageManager.lowercase(Locale.getDefault())) + else -> null } } fun details(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.additionalData.details ?: "" - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> this.additionalData.details ?: "" - else -> TODO() + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> this.additionalData.details ?: "" + else -> this.additionalData.details ?: "" } } fun annotationMessage(): String { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> this.title + " in " + this.additionalData.packageName + " id: " + this.ruleId() - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> + return when (this.filterableIssueType) { + FilterableIssueType.OpenSource -> this.title + " in " + this.additionalData.packageName + " id: " + this.ruleId() + else -> this.title.ifBlank { this.additionalData.message.let { if (it.length < 70) it else "${it.take(70)}..." } + " (Snyk)" } - else -> TODO() } } - fun canLoadSuggestionPanelFromHTML(): Boolean { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> true - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> this.additionalData.details != null - - else -> TODO() - } - } + fun canLoadSuggestionPanelFromHTML(): Boolean = true fun hasAIFix(): Boolean { - return when (this.additionalData.getProductType()) { - ProductType.OSS -> - this.additionalData.isUpgradable - - ProductType.CODE_SECURITY, ProductType.CODE_QUALITY -> { - this.additionalData.hasAIFix - } - else -> TODO() - } + return this.additionalData.isUpgradable || this.hasAIFix() } - fun isIgnored(): Boolean { - return pluginSettings().isGlobalIgnoresFeatureEnabled && this.isIgnored == true - } + fun isIgnored(): Boolean = this.isIgnored == true fun getSeverityAsEnum(): Severity { return when (severity) { @@ -417,6 +402,8 @@ data class DataFlow( ) data class IssueData( + // all + @SerializedName("key") val key: String, // Code @SerializedName("message") val message: String, @SerializedName("leadURL") val leadURL: String?, @@ -457,20 +444,6 @@ data class IssueData( @SerializedName("ruleId") val ruleId: String, @SerializedName("details") val details: String?, ) { - fun getProductType(): ProductType { - // TODO: how else to differentiate? - if (this.packageManager != null) { - return ProductType.OSS - } - if (this.message != null) { - return if (this.isSecurityType) { - ProductType.CODE_SECURITY - } else { - ProductType.CODE_QUALITY - } - } - throw IllegalStateException("Not defined type of IssueData") - } override fun equals(other: Any?): Boolean { if (this === other) return true @@ -478,106 +451,11 @@ data class IssueData( other as IssueData - if (this.getProductType() == ProductType.OSS) { - if (ruleId != other.ruleId) return false - if (license != other.license) return false - if (identifiers != other.identifiers) return false - if (description != other.description) return false - if (language != other.language) return false - if (packageManager != other.packageManager) return false - if (packageName != other.packageName) return false - if (name != other.name) return false - if (version != other.version) return false - if (exploit != other.exploit) return false - if (CVSSv3 != other.CVSSv3) return false - if (cvssScore != other.cvssScore) return false - if (fixedIn != other.fixedIn) return false - if (from != other.from) return false - if (upgradePath != other.upgradePath) return false - if (isPatchable != other.isPatchable) return false - if (isUpgradable != other.isUpgradable) return false - if (projectName != other.projectName) return false - if (displayTargetFile != other.displayTargetFile) return false - if (matchingIssues != other.matchingIssues) return false - if (lesson != other.lesson) return false - if (details != other.details) return false - - return true - } - - if (message != other.message) return false - if (leadURL != other.leadURL) return false - if (rule != other.rule) return false - if (ruleId != other.ruleId) return false - if (repoDatasetSize != other.repoDatasetSize) return false - if (exampleCommitFixes != other.exampleCommitFixes) return false - if (cwe != other.cwe) return false - if (text != other.text) return false - if (markers != other.markers) return false - if (cols != null) { - if (other.cols == null) return false - if (!cols.contentEquals(other.cols)) return false - } else if (other.cols != null) { - return false - } - if (rows != null) { - if (other.rows == null) return false - if (!rows.contentEquals(other.rows)) return false - } else if (other.rows != null) { - return false - } - if (isSecurityType != other.isSecurityType) return false - if (priorityScore != other.priorityScore) return false - if (hasAIFix != other.hasAIFix) return false - if (dataFlow != other.dataFlow) return false - if (details != other.details) return false - - return true + return this.key == other.key } override fun hashCode(): Int { - if (this.getProductType() == ProductType.OSS) { - var result = ruleId.hashCode() - result = 31 * result + description.hashCode() - result = 31 * result + (license?.hashCode() ?: 0) - result = 31 * result + (identifiers?.hashCode() ?: 0) - result = 31 * result + language.hashCode() - result = 31 * result + packageManager.hashCode() - result = 31 * result + packageName.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + version.hashCode() - result = 31 * result + (exploit?.hashCode() ?: 0) - result = 31 * result + (CVSSv3?.hashCode() ?: 0) - result = 31 * result + (cvssScore?.hashCode() ?: 0) - result = 31 * result + (fixedIn?.hashCode() ?: 0) - result = 31 * result + from.hashCode() - result = 31 * result + upgradePath.hashCode() - result = 31 * result + isPatchable.hashCode() - result = 31 * result + (isUpgradable?.hashCode() ?: 0) - result = 31 * result + displayTargetFile.hashCode() - result = 31 * result + (details?.hashCode() ?: 0) - result = 31 * result + (matchingIssues?.hashCode() ?: 0) - result = 31 * result + (lesson?.hashCode() ?: 0) - return result - } - - var result = message.hashCode() - result = 31 * result + (leadURL?.hashCode() ?: 0) - result = 31 * result + rule.hashCode() - result = 31 * result + ruleId.hashCode() - result = 31 * result + repoDatasetSize - result = 31 * result + exampleCommitFixes.hashCode() - result = 31 * result + (cwe?.hashCode() ?: 0) - result = 31 * result + text.hashCode() - result = 31 * result + (markers?.hashCode() ?: 0) - result = 31 * result + (cols?.contentHashCode() ?: 0) - result = 31 * result + (rows?.contentHashCode() ?: 0) - result = 31 * result + isSecurityType.hashCode() - result = 31 * result + priorityScore - result = 31 * result + hasAIFix.hashCode() - result = 31 * result + dataFlow.hashCode() - result = 31 * result + details.hashCode() - return result + return this.key.hashCode() } } @@ -597,6 +475,7 @@ data class IgnoreDetails( @SerializedName("ignoredBy") val ignoredBy: String, ) +@Suppress("PropertyName") data class OssIdentifiers( @SerializedName("CWE") val CWE: List?, @SerializedName("CVE") val CVE: List?, diff --git a/src/main/kotlin/snyk/trust/TrustedProjects.kt b/src/main/kotlin/snyk/trust/TrustedProjects.kt index 96cd62c79..1b66ee6b0 100644 --- a/src/main/kotlin/snyk/trust/TrustedProjects.kt +++ b/src/main/kotlin/snyk/trust/TrustedProjects.kt @@ -3,6 +3,7 @@ package snyk.trust import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeAndWaitIfNeeded import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger @@ -58,7 +59,7 @@ private fun confirmScanningUntrustedProject(project: Project): ScanUntrustedProj var choice = ScanUntrustedProjectChoice.CANCEL - runInEdt { + invokeAndWaitIfNeeded { val result = MessageDialogBuilder .yesNo(title, message) .icon(Messages.getWarningIcon()) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e2c8a9936..d3906dcb3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -50,9 +50,9 @@ - - + + 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 31c6b625f..65f4b8c63 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -41,6 +41,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { private lateinit var rootOssIssuesTreeNode: DefaultMutableTreeNode private lateinit var rootSecurityIssuesTreeNode: DefaultMutableTreeNode private lateinit var rootQualityIssuesTreeNode: DefaultMutableTreeNode + private lateinit var rootIacIssuesTreeNode: DefaultMutableTreeNode private val fileName = "app.js" private lateinit var file: VirtualFile @@ -152,6 +153,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootSecurityIssuesTreeNode, rootQualityIssuesTreeNode, rootOssIssuesTreeNode, + rootIacIssuesTreeNode ) TestCase.assertEquals(3, rootTreeNode.childCount) @@ -189,6 +191,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootSecurityIssuesTreeNode, rootQualityIssuesTreeNode, rootOssIssuesTreeNode, + rootIacIssuesTreeNode ) TestCase.assertEquals(3, rootTreeNode.childCount) @@ -230,6 +233,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootSecurityIssuesTreeNode, rootQualityIssuesTreeNode, rootOssIssuesTreeNode, + rootIacIssuesTreeNode ) TestCase.assertEquals(3, rootTreeNode.childCount) @@ -257,6 +261,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootSecurityIssuesTreeNode, rootQualityIssuesTreeNode, rootOssIssuesTreeNode, + rootIacIssuesTreeNode ) TestCase.assertEquals(3, rootTreeNode.childCount) From 1878aedd0d8d6b69e44bbd231b80f1e001ec38e3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 20 Sep 2024 13:29:42 +0200 Subject: [PATCH 25/51] feat: add annotator (wip) --- src/main/kotlin/io/snyk/plugin/Utils.kt | 39 ++++++---- .../toolwindow/panels/JCEFDescriptionPanel.kt | 14 ++-- .../snyk/common/annotator/SnykIaCAnnotator.kt | 28 +++++++ src/main/kotlin/snyk/common/lsp/Types.kt | 75 +++++++++---------- src/main/resources/META-INF/plugin.xml | 3 + 5 files changed, 98 insertions(+), 61 deletions(-) create mode 100644 src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index c7cf7793c..61e56ba08 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -158,13 +158,24 @@ fun isUrlValid(url: String?): Boolean { } fun isOssRunning(project: Project): Boolean { + return isProductScanRunning(project, ProductType.OSS, getSnykTaskQueueService(project)?.ossScanProgressIndicator) +} + +private fun isProductScanRunning(project: Project, productType: ProductType): Boolean { + return isProductScanRunning(project, productType, null) +} + +private fun isProductScanRunning( + project: Project, + productType: ProductType, + progressIndicator: ProgressIndicator? +): Boolean { val lsRunning = project.getContentRootVirtualFiles().any { vf -> - val key = ScanInProgressKey(vf, ProductType.OSS) + val key = ScanInProgressKey(vf, productType) ScanState.scanInProgress[key] == true } - val indicator = getSnykTaskQueueService(project)?.ossScanProgressIndicator return lsRunning || - (indicator != null && indicator.isRunning && !indicator.isCanceled) + (progressIndicator != null && progressIndicator.isRunning && !progressIndicator.isCanceled) } fun cancelOssIndicator(project: Project) { @@ -178,22 +189,22 @@ fun cancelIacIndicator(project: Project) { } fun isSnykCodeRunning(project: Project): Boolean { - val lsRunning = project.getContentRootVirtualFiles().any { vf -> - val key = ScanInProgressKey(vf, ProductType.CODE_SECURITY) - ScanState.scanInProgress[key] == true - } - - return lsRunning + return isProductScanRunning(project, ProductType.CODE_SECURITY) || isProductScanRunning( + project, + ProductType.CODE_QUALITY + ) } fun isIacRunning(project: Project): Boolean { - val indicator = getSnykTaskQueueService(project)?.iacScanProgressIndicator - return indicator != null && indicator.isRunning && !indicator.isCanceled + return isProductScanRunning(project, ProductType.IAC, getSnykTaskQueueService(project)?.iacScanProgressIndicator) } fun isContainerRunning(project: Project): Boolean { - val indicator = getSnykTaskQueueService(project)?.containerScanProgressIndicator - return indicator != null && indicator.isRunning && !indicator.isCanceled + return isProductScanRunning( + project, + ProductType.CONTAINER, + getSnykTaskQueueService(project)?.containerScanProgressIndicator + ) } fun isScanRunning(project: Project): Boolean = @@ -238,7 +249,7 @@ fun findPsiFileIgnoringExceptions(virtualFile: VirtualFile, project: Project): P null } else { try { - var psiFile : PsiFile? = null + var psiFile: PsiFile? = null ReadAction.run { psiFile = PsiManager.getInstance(project).findFile(virtualFile) } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index cbf71b59d..a2d07a8a2 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -50,8 +50,8 @@ class SuggestionDescriptionPanelFromLS( ThemeBasedStylingGenerator().generate(it) } - if (issue.filterableIssueType == FilterableIssueType.CodeQuality || - issue.filterableIssueType == FilterableIssueType.CodeSecurity + if (issue.filterableIssueType == ScanIssue.CODE_QUALITY || + issue.filterableIssueType == ScanIssue.CODE_SECURITY ) { val virtualFiles = LinkedHashMap() for (dataFlow in issue.additionalData.dataFlow) { @@ -119,8 +119,8 @@ class SuggestionDescriptionPanelFromLS( JPanel( GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20), ).apply { - if (issue.filterableIssueType == FilterableIssueType.CodeSecurity || - issue.filterableIssueType == FilterableIssueType.CodeQuality + if (issue.filterableIssueType == ScanIssue.CODE_SECURITY || + issue.filterableIssueType == ScanIssue.CODE_QUALITY ) { this.add( SnykCodeOverviewPanel(issue.additionalData), @@ -134,7 +134,7 @@ class SuggestionDescriptionPanelFromLS( SnykCodeExampleFixesPanel(issue.additionalData), panelGridConstraints(4), ) - } else if (issue.filterableIssueType == FilterableIssueType.OpenSource) { + } else if (issue.filterableIssueType == ScanIssue.OPEN_SOURCE) { this.add( SnykOSSIntroducedThroughPanel(issue.additionalData), baseGridConstraintsAnchorWest(1, indent = 0), @@ -157,8 +157,8 @@ class SuggestionDescriptionPanelFromLS( fun getCustomCssAndScript(): String { var html = issue.details() val ideScript = getCustomScript() - val ideStyle: String = if (issue.filterableIssueType == FilterableIssueType.CodeSecurity || - issue.filterableIssueType == FilterableIssueType.CodeQuality + val ideStyle: String = if (issue.filterableIssueType == ScanIssue.CODE_SECURITY || + issue.filterableIssueType == ScanIssue.CODE_QUALITY ) { SnykStylesheets.SnykCodeSuggestion } else { diff --git a/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt new file mode 100644 index 000000000..de17f4237 --- /dev/null +++ b/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt @@ -0,0 +1,28 @@ +package snyk.common.annotator + +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.openapi.util.Disposer +import com.intellij.psi.PsiFile +import io.snyk.plugin.isIacRunning +import io.snyk.plugin.isOssRunning +import io.snyk.plugin.pluginSettings +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import snyk.common.ProductType + +class SnykIaCAnnotator : SnykAnnotator(product = ProductType.OSS) { + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + + override fun apply( + psiFile: PsiFile, + annotationResult: List, + holder: AnnotationHolder, + ) { + if (disposed) return + if (!pluginSettings().ossScanEnable) return + if (isIacRunning(psiFile.project)) return + + super.apply(psiFile, annotationResult, holder) + } +} diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 3ced9b917..ea362925e 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -8,22 +8,17 @@ import com.intellij.openapi.util.TextRange import com.intellij.openapi.vfs.VirtualFile import io.snyk.plugin.Severity import io.snyk.plugin.getDocument -import io.snyk.plugin.pluginSettings import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.PackageManagerIconProvider.Companion.getIcon import org.eclipse.lsp4j.Range -import snyk.common.ProductType import java.util.Date import java.util.Locale import javax.swing.Icon -// type CliOutput struct { -// Code int `json:"code,omitempty"` -// ErrorMessage string `json:"error,omitempty"` -// Path string `json:"path,omitempty"` -// Command string `json:"command,omitempty"` -//} -data class CliError ( + +typealias FilterableIssueType = String + +data class CliError( val code: Int? = 0, val error: String? = null, val path: String? = null, @@ -57,15 +52,6 @@ enum class LsProductConstants(val value: String) { } } -enum class FilterableIssueType(val value: String) { - OpenSource("Open Source"), - CodeQuality("Code Quality"), - CodeSecurity("Code Security"), - InfrastructureAsCode("Infrastructure As Code"), - Container("Container") -} - - // Define the ScanIssue data class data class ScanIssue( val id: String, // Unique key identifying an issue in the whole result set. Not the same as the Snyk issue ID. @@ -79,6 +65,15 @@ data class ScanIssue( var filterableIssueType: FilterableIssueType, var ignoreDetails: IgnoreDetails?, ) : Comparable { + + companion object { + const val OPEN_SOURCE: FilterableIssueType = "Open Source" + const val CODE_SECURITY: FilterableIssueType = "Code Security" + const val CODE_QUALITY: FilterableIssueType = "Code Quality" + const val INFRASTRUCTURE_AS_CODE: FilterableIssueType = "Infrastructure As Code" + const val CONTAINER: FilterableIssueType = "Container" + } + var textRange: TextRange? = null get() { return if (startOffset == null || endOffset == null) { @@ -137,12 +132,12 @@ data class ScanIssue( fun title(): String { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> this.title - FilterableIssueType.CodeQuality -> { + OPEN_SOURCE, INFRASTRUCTURE_AS_CODE -> this.title + CODE_QUALITY -> { this.additionalData.message.split('.').firstOrNull() ?: "Unknown issue" } - FilterableIssueType.CodeSecurity -> { + CODE_SECURITY -> { this.title.split(":").firstOrNull() ?: "Unknown issue" } @@ -152,11 +147,11 @@ data class ScanIssue( fun longTitle(): String { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> { + OPEN_SOURCE -> { "${this.additionalData.packageName}@${this.additionalData.version}: ${this.title()}" } - FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity, FilterableIssueType.InfrastructureAsCode -> { + CODE_QUALITY, CODE_SECURITY, INFRASTRUCTURE_AS_CODE -> { return "${this.title()} [${this.range.start.line + 1},${this.range.start.character}]" } @@ -166,7 +161,7 @@ data class ScanIssue( fun priority(): Int { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> { + OPEN_SOURCE, INFRASTRUCTURE_AS_CODE -> { return when (this.getSeverityAsEnum()) { Severity.CRITICAL -> 4 Severity.HIGH -> 3 @@ -176,14 +171,14 @@ data class ScanIssue( } } - FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity -> this.additionalData.priorityScore + CODE_QUALITY, CODE_SECURITY -> this.additionalData.priorityScore else -> TODO() } } fun issueNaming(): String { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> { + OPEN_SOURCE -> { if (this.additionalData.license != null) { "License" } else { @@ -191,20 +186,20 @@ data class ScanIssue( } } - FilterableIssueType.CodeSecurity -> "Security Issue" - FilterableIssueType.CodeQuality -> "Quality Issue" - FilterableIssueType.InfrastructureAsCode -> "Configuration Issue" + CODE_SECURITY -> "Security Issue" + CODE_QUALITY -> "Quality Issue" + INFRASTRUCTURE_AS_CODE -> "Configuration Issue" else -> TODO() } } fun cwes(): List { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> { + OPEN_SOURCE, INFRASTRUCTURE_AS_CODE -> { this.additionalData.identifiers?.CWE ?: emptyList() } - FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity -> { + CODE_QUALITY, CODE_SECURITY -> { this.additionalData.cwe ?: emptyList() } @@ -214,32 +209,32 @@ data class ScanIssue( fun cves(): List { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> { + OPEN_SOURCE -> { this.additionalData.identifiers?.CVE ?: emptyList() } - FilterableIssueType.CodeQuality, FilterableIssueType.CodeSecurity, FilterableIssueType.InfrastructureAsCode -> emptyList() + CODE_QUALITY, CODE_SECURITY, INFRASTRUCTURE_AS_CODE -> emptyList() else -> TODO() } } fun cvssScore(): String? { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> this.additionalData.cvssScore + OPEN_SOURCE -> this.additionalData.cvssScore else -> null } } fun cvssV3(): String? { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> this.additionalData.CVSSv3 + OPEN_SOURCE -> this.additionalData.CVSSv3 else -> null } } fun id(): String? { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource, FilterableIssueType.InfrastructureAsCode -> this.additionalData.ruleId + OPEN_SOURCE, INFRASTRUCTURE_AS_CODE -> this.additionalData.ruleId else -> null } } @@ -250,21 +245,21 @@ data class ScanIssue( fun icon(): Icon? { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> getIcon(this.additionalData.packageManager.lowercase(Locale.getDefault())) + OPEN_SOURCE -> getIcon(this.additionalData.packageManager.lowercase(Locale.getDefault())) else -> null } } fun details(): String { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> this.additionalData.details ?: "" + OPEN_SOURCE -> this.additionalData.details ?: "" else -> this.additionalData.details ?: "" } } fun annotationMessage(): String { return when (this.filterableIssueType) { - FilterableIssueType.OpenSource -> this.title + " in " + this.additionalData.packageName + " id: " + this.ruleId() + OPEN_SOURCE -> this.title + " in " + this.additionalData.packageName + " id: " + this.ruleId() else -> this.title.ifBlank { this.additionalData.message.let { @@ -278,7 +273,7 @@ data class ScanIssue( fun canLoadSuggestionPanelFromHTML(): Boolean = true fun hasAIFix(): Boolean { - return this.additionalData.isUpgradable || this.hasAIFix() + return this.additionalData.isUpgradable || this.additionalData.hasAIFix } fun isIgnored(): Boolean = this.isIgnored == true diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d3906dcb3..318d498f7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,3 +1,4 @@ + io.snyk.snyk-intellij-plugin Snyk Security @@ -52,10 +53,12 @@ + + From e8fed01b0b22ecb51f63cad2ec4910e634c13122 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 20 Sep 2024 18:15:49 +0200 Subject: [PATCH 26/51] feat: format iac, refactor jcefDescriptionPanel (wip) --- .../SnykApplicationSettingsStateService.kt | 6 +- .../SnykProjectSettingsConfigurable.kt | 9 +- .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 4 +- .../ui/jcef/ThemeBasedStylingGenerator.kt | 9 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 20 ++- .../SnykToolWindowSnykScanListenerLS.kt | 12 +- .../nodes/secondlevel/InfoTreeNode.kt | 2 +- .../panels/IssueDescriptionPanelBase.kt | 2 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 143 ++++++++++-------- .../snyk/common/annotator/SnykAnnotator.kt | 4 +- .../snyk/common/annotator/SnykIaCAnnotator.kt | 5 +- src/main/kotlin/snyk/common/lsp/Types.kt | 17 ++- .../SnykToolWindowSnykScanListenerLSTest.kt | 4 - 13 files changed, 135 insertions(+), 102 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 24c55bb6d..437f07287 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -35,7 +35,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent().getAll() .values.joinToString("\n") { "Base branch for ${it.folderPath}: ${it.baseBranch}" } - netNewIssuesDropDown.selectedItem = applicationSettings.netNewIssues + netNewIssuesDropDown.selectedItem = applicationSettings.displayAllIssues } } @@ -815,7 +815,7 @@ class SnykSettingsDialog( fun getCliReleaseChannel(): String = cliReleaseChannelDropDown.selectedItem as String - fun getNetNewIssuesSelected(): String = netNewIssuesDropDown.selectedItem as String + fun getDisplayIssuesSelection(): String = netNewIssuesDropDown.selectedItem as String fun getUseTokenAuthentication(): Boolean = useTokenAuthentication.selectedIndex == 1 } diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt index f50caa3f9..40eca090d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ThemeBasedStylingGenerator.kt @@ -5,16 +5,19 @@ import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.util.ui.JBUI -import com.intellij.ui.JBColor import com.intellij.util.ui.UIUtil import org.cef.browser.CefBrowser import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import java.awt.Color +fun Color.toHex() = ThemeBasedStylingGenerator.toCssHex(this) + class ThemeBasedStylingGenerator { - fun toCssHex(color: Color): String { - return "#%02x%02x%02x".format(color.red, color.green, color.blue) + companion object { + fun toCssHex(color: Color): String { + return "#%02x%02x%02x".format(color.red, color.green, color.blue) + } } fun shift( 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 44ff98fd9..46a828ada 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -5,6 +5,7 @@ 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.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.openapi.ui.SimpleToolWindowPanel @@ -66,6 +67,7 @@ import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly import snyk.common.ProductType import snyk.common.SnykError +import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import snyk.container.ContainerIssuesForImage @@ -94,9 +96,9 @@ class SnykToolWindowPanel( val project: Project, ) : JPanel(), Disposable { - internal val descriptionPanel = SimpleToolWindowPanel(true, true).apply { name = "descriptionPanel" } + private val descriptionPanel = SimpleToolWindowPanel(true, true).apply { name = "descriptionPanel" } private val logger = Logger.getInstance(this::class.java) - private val rootTreeNode = DefaultMutableTreeNode("") + private val rootTreeNode = ChooseBranchNode(project = project) private val rootOssTreeNode = RootOssTreeNode(project) private val rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) private val rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) @@ -110,10 +112,14 @@ class SnykToolWindowPanel( rootTreeNode.add(rootIacIssuesTreeNode) rootTreeNode.add(rootContainerIssuesTreeNode) Tree(rootTreeNode).apply { - this.isRootVisible = false + this.isRootVisible = pluginSettings().isDeltaFindingsEnabled() } } + private fun getRootNodeText(folderPath: String, baseBranch: String) = + "Click to choose base branch for $folderPath [ current: $baseBranch ]" + + /** Flag used to recognize not-user-initiated Description panel reload cases for purposes like: * - disable Itly logging * - don't navigate to source (in the Editor) @@ -127,7 +133,13 @@ class SnykToolWindowPanel( override fun getSnykError(): SnykError? = null } + + init { + val folderConfig = service().getFolderConfig(project.basePath.toString()) + val rootNodeText = folderConfig?.let { getRootNodeText(it.folderPath, it.baseBranch) } ?: "Choose branch on ${project.basePath}" + rootTreeNode.info = rootNodeText + vulnerabilitiesTree.cellRenderer = SnykTreeCellRenderer() layout = BorderLayout() @@ -437,8 +449,6 @@ class SnykToolWindowPanel( try { enableCodeScanAccordingToServerSetting() displayEmptyDescription() - // don't trigger scan for Default project i.e. no project opened state - if (project.basePath != null) triggerScan() } catch (e: Exception) { displaySnykError(SnykError(e.message ?: "Exception while initializing plugin {${e.message}", "")) logger.error("Failed to apply Snyk settings", e) 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 7028da6bb..19aba3824 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -34,6 +34,7 @@ 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.FolderConfig import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams @@ -304,17 +305,6 @@ class SnykToolWindowSnykScanListenerLS( } val settings = pluginSettings() - if (settings.isDeltaFindingsEnabled()) { - // we need one choose branch node for each content root. sigh. - service().getAllForProject(project).forEach { - val branchChooserTreeNode = ChooseBranchNode( - project = project, - info = "Click to choose base branch for ${it.folderPath} [ current: ${it.baseBranch} ]" - ) - rootNode.add(branchChooserTreeNode) - } - } - var text = "✅ Congrats! No vulnerabilities found!" val issuesCount = issues.size val ignoredIssuesCount = issues.count { it.isIgnored() } 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 4a0516029..797a92a9c 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 @@ -9,6 +9,6 @@ open class InfoTreeNode( open val project: Project, ) : DefaultMutableTreeNode(info) -class ChooseBranchNode(override val info: String = "Choose base branch", override val project: Project) : InfoTreeNode(info, project) { +class ChooseBranchNode(override var info: String = "Choose base branch", override var project: Project) : InfoTreeNode(info, project) { val icon = AllIcons.Vcs.BranchNode } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt index 6cad0109d..8b11be74e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/IssueDescriptionPanelBase.kt @@ -50,7 +50,7 @@ abstract class IssueDescriptionPanelBase( add(mainBodyPanel, BorderLayout.CENTER) } - fun titlePanel(insets: Insets = JBUI.insets(20, 10, 20, 20), indent: Int = 1): JPanel { + private fun titlePanel(insets: Insets = JBUI.insets(20, 10, 20, 20), indent: Int = 1): JPanel { val titlePanel = JPanel() titlePanel.layout = GridLayoutManager(2, 2, insets, -1, 5) val titleLabel = JLabel().apply { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index a2d07a8a2..99c709fdb 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -1,5 +1,7 @@ package io.snyk.plugin.ui.toolwindow.panels +import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.colors.EditorColorsManager import io.snyk.plugin.ui.jcef.GenerateAIFixHandler import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager @@ -16,11 +18,10 @@ import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.jcef.LoadHandlerGenerator import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator import io.snyk.plugin.ui.jcef.ThemeBasedStylingGenerator +import io.snyk.plugin.ui.jcef.toHex import io.snyk.plugin.ui.panelGridConstraints import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel import io.snyk.plugin.ui.wrapWithScrollPane -import snyk.common.ProductType -import snyk.common.lsp.FilterableIssueType import snyk.common.lsp.ScanIssue import stylesheets.SnykStylesheets import java.awt.BorderLayout @@ -46,32 +47,33 @@ class SuggestionDescriptionPanelFromLS( val loadHandlerGenerators: MutableList = emptyList().toMutableList() + // TODO: replace directly in HTML instead of JS loadHandlerGenerators += { ThemeBasedStylingGenerator().generate(it) } - if (issue.filterableIssueType == ScanIssue.CODE_QUALITY || - issue.filterableIssueType == ScanIssue.CODE_SECURITY - ) { - val virtualFiles = LinkedHashMap() - for (dataFlow in issue.additionalData.dataFlow) { - virtualFiles[dataFlow.filePath] = - VirtualFileManager.getInstance().findFileByNioPath(Paths.get(dataFlow.filePath)) - } + when (issue.filterableIssueType) { + ScanIssue.CODE_QUALITY, ScanIssue.CODE_SECURITY -> { + val virtualFiles = LinkedHashMap() + for (dataFlow in issue.additionalData.dataFlow) { + virtualFiles[dataFlow.filePath] = + VirtualFileManager.getInstance().findFileByNioPath(Paths.get(dataFlow.filePath)) + } - val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile.project, virtualFiles) - loadHandlerGenerators += { - openFileLoadHandlerGenerator.generate(it) - } + val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile.project, virtualFiles) + loadHandlerGenerators += { + openFileLoadHandlerGenerator.generate(it) + } - val generateAIFixHandler = GenerateAIFixHandler() - loadHandlerGenerators += { - generateAIFixHandler.generateAIFixCommand(it) - } + val generateAIFixHandler = GenerateAIFixHandler() + loadHandlerGenerators += { + generateAIFixHandler.generateAIFixCommand(it) + } - val applyFixHandler = ApplyFixHandler(snykFile.project) - loadHandlerGenerators += { - applyFixHandler.generateApplyFixCommand(it) + val applyFixHandler = ApplyFixHandler(snykFile.project) + loadHandlerGenerators += { + applyFixHandler.generateApplyFixCommand(it) + } } } val html = this.getCustomCssAndScript() @@ -119,61 +121,80 @@ class SuggestionDescriptionPanelFromLS( JPanel( GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20), ).apply { - if (issue.filterableIssueType == ScanIssue.CODE_SECURITY || - issue.filterableIssueType == ScanIssue.CODE_QUALITY - ) { - this.add( - SnykCodeOverviewPanel(issue.additionalData), - panelGridConstraints(2), - ) - this.add( - SnykCodeDataflowPanel(project, issue.additionalData), - panelGridConstraints(3), - ) - this.add( - SnykCodeExampleFixesPanel(issue.additionalData), - panelGridConstraints(4), - ) - } else if (issue.filterableIssueType == ScanIssue.OPEN_SOURCE) { - this.add( - SnykOSSIntroducedThroughPanel(issue.additionalData), - baseGridConstraintsAnchorWest(1, indent = 0), - ) - this.add( - SnykOSSDetailedPathsPanel(issue.additionalData), - panelGridConstraints(2), - ) - this.add( - SnykOSSOverviewPanel(issue.additionalData), - panelGridConstraints(3), - ) - } else { - // do nothing + when (issue.filterableIssueType) { + ScanIssue.CODE_SECURITY, ScanIssue.CODE_QUALITY -> { + this.add( + SnykCodeOverviewPanel(issue.additionalData), + panelGridConstraints(2), + ) + this.add( + SnykCodeDataflowPanel(project, issue.additionalData), + panelGridConstraints(3), + ) + this.add( + SnykCodeExampleFixesPanel(issue.additionalData), + panelGridConstraints(4), + ) + } + + ScanIssue.OPEN_SOURCE -> { + this.add( + SnykOSSIntroducedThroughPanel(issue.additionalData), + baseGridConstraintsAnchorWest(1, indent = 0), + ) + this.add( + SnykOSSDetailedPathsPanel(issue.additionalData), + panelGridConstraints(2), + ) + this.add( + SnykOSSOverviewPanel(issue.additionalData), + panelGridConstraints(3), + ) + } + + else -> { + // do nothing + } } } return Pair(panel, lastRowToAddSpacer) } fun getCustomCssAndScript(): String { - var html = issue.details() + var html = issue.details().ifBlank { issue.additionalData.customUIContent } val ideScript = getCustomScript() - val ideStyle: String = if (issue.filterableIssueType == ScanIssue.CODE_SECURITY || - issue.filterableIssueType == ScanIssue.CODE_QUALITY - ) { - SnykStylesheets.SnykCodeSuggestion - } else { - SnykStylesheets.SnykOSSSuggestion + + // TODO: remove custom stylesheets, deliver variables from LS, replace variables with colors + val ideStyle: String = when (issue.filterableIssueType) { + ScanIssue.CODE_SECURITY, ScanIssue.CODE_QUALITY -> SnykStylesheets.SnykCodeSuggestion + ScanIssue.OPEN_SOURCE -> SnykStylesheets.SnykOSSSuggestion + else -> "" } + val editorColorsManager = EditorColorsManager.getInstance() + val editorUiTheme = editorColorsManager.schemeForCurrentUITheme + html = html.replace("\${ideStyle}", "") html = html.replace("\${headerEnd}", "") html = html.replace("\${ideScript}", "") + val nonce = getNonce() html = html.replace("\${nonce}", nonce) - val fontUpdate = "--default-font: \"${UIUtil.getLabelFont().fontName}\", " - html = html.replace("--default-font:", fontUpdate) - + html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().fontName}\", ") + html = html.replace("var(--text-color)", UIUtil.getLabelForeground().toHex()) + html = html.replace("var(--background-color)", UIUtil.getPanelBackground().toHex()) + html = + html.replace("var(--border-color)", JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground().toHex()) + html = html.replace("var(--link-color)", JBUI.CurrentTheme.Link.Foreground.ENABLED.toHex()) + html = html.replace( + "var(--horizontal-border-color)", + JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground().toHex() + ) + html = html.replace( + "var(--code-background-color)", + editorUiTheme.getColor(EditorColors.GUTTER_BACKGROUND)?.toHex() ?: editorUiTheme.defaultBackground.toHex() + ) return html } diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index f482120a5..176b0079a 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -247,7 +247,9 @@ abstract class SnykAnnotator(private val product: ProductType) : private fun getIssuesForFile(psiFile: PsiFile): Set = getSnykCachedResultsForProduct(psiFile.project, product) - ?.filter { it.key.virtualFile == psiFile.virtualFile } + ?.filter { + it.key.virtualFile == psiFile.virtualFile + } ?.map { it.value } ?.flatten() ?.toSet() diff --git a/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt index de17f4237..dee23cb45 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykIaCAnnotator.kt @@ -4,12 +4,11 @@ import com.intellij.lang.annotation.AnnotationHolder import com.intellij.openapi.util.Disposer import com.intellij.psi.PsiFile import io.snyk.plugin.isIacRunning -import io.snyk.plugin.isOssRunning import io.snyk.plugin.pluginSettings import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.common.ProductType -class SnykIaCAnnotator : SnykAnnotator(product = ProductType.OSS) { +class SnykIaCAnnotator : SnykAnnotator(product = ProductType.IAC) { init { Disposer.register(SnykPluginDisposable.getInstance(), this) } @@ -20,7 +19,7 @@ class SnykIaCAnnotator : SnykAnnotator(product = ProductType.OSS) { holder: AnnotationHolder, ) { if (disposed) return - if (!pluginSettings().ossScanEnable) return + if (!pluginSettings().iacScanEnabled) return if (isIacRunning(psiFile.project)) return super.apply(psiFile, annotationResult, holder) diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index ea362925e..550edbe9c 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -396,9 +396,8 @@ data class DataFlow( @SerializedName("content") val content: String, ) +@Suppress("PropertyName") data class IssueData( - // all - @SerializedName("key") val key: String, // Code @SerializedName("message") val message: String, @SerializedName("leadURL") val leadURL: String?, @@ -435,7 +434,19 @@ data class IssueData( @SerializedName("displayTargetFile") val displayTargetFile: String?, @SerializedName("matchingIssues") val matchingIssues: List, @SerializedName("lesson") val lesson: String?, - // Code and OSS + // IAC + @SerializedName("publicId") val publicId: String, + @SerializedName("documentation") val documentation: String, + @SerializedName("lineNumber") val lineNumber: String, + @SerializedName("issue") val issue: String, + @SerializedName("impact") val impact: String, + @SerializedName("resolve") val resolve: String, + @SerializedName("path") val path: List, + @SerializedName("references") val references: List, + @SerializedName("customUIContent") val customUIContent: String, + + // all + @SerializedName("key") val key: String, @SerializedName("ruleId") val ruleId: String, @SerializedName("details") val details: String?, ) { 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 65f4b8c63..0fa9bc0a7 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -1,6 +1,5 @@ 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 @@ -12,12 +11,10 @@ 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 @@ -26,7 +23,6 @@ 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 From 5e1207738c6d3fe68f473ea86ad46503027fc9b8 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Tue, 24 Sep 2024 18:34:56 +0100 Subject: [PATCH 27/51] further wip --- src/main/kotlin/io/snyk/plugin/Utils.kt | 2 + .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 2 +- .../toolwindow/panels/JCEFDescriptionPanel.kt | 2 +- .../kotlin/snyk/common/SnykCachedResults.kt | 10 +- .../snyk/common/annotator/SnykAnnotator.kt | 1 + .../snyk/common/lsp/LanguageServerWrapper.kt | 6 +- .../snyk/common/lsp/SnykLanguageClient.kt | 165 +----------------- 7 files changed, 23 insertions(+), 165 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 61e56ba08..92fe66bb2 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -61,6 +61,8 @@ import java.net.URI import java.nio.file.Path import java.util.Objects.nonNull import java.util.SortedSet +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap import java.util.concurrent.TimeUnit import javax.swing.JComponent diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt index 273027b5e..c1de09f89 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -37,7 +37,7 @@ object JCEFUtils { val cefClient = JBCefApp.getInstance().createClient() cefClient.setProperty("JS_QUERY_POOL_SIZE", 1) val jbCefBrowser = - JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(false) + JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(true) .setMouseWheelEventEnable(true).build() jbCefBrowser.setOpenLinksInExternalBrowser(true) val jbCefPair = Pair(cefClient, jbCefBrowser) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 99c709fdb..5cf05a08e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -181,7 +181,7 @@ class SuggestionDescriptionPanelFromLS( val nonce = getNonce() html = html.replace("\${nonce}", nonce) - html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().fontName}\", ") + html = html.replace("--default-font: ", "--default-font: \"${JBUI.Fonts.label().asPlain().family}\", ") html = html.replace("var(--text-color)", UIUtil.getLabelForeground().toHex()) html = html.replace("var(--background-color)", UIUtil.getPanelBackground().toHex()) html = diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 6ce2b688b..395e63c00 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer +import io.ktor.util.collections.ConcurrentMap import io.snyk.plugin.Severity import io.snyk.plugin.SnykFile import io.snyk.plugin.events.SnykScanListener @@ -19,6 +20,7 @@ import snyk.common.lsp.SnykScanParams import snyk.container.ContainerResult import snyk.container.ContainerService import snyk.iac.IacResult +import java.util.concurrent.ConcurrentHashMap @Service(Service.Level.PROJECT) class SnykCachedResults( @@ -40,14 +42,14 @@ class SnykCachedResults( fun isDisposed() = disposed - val currentSnykCodeResultsLS: MutableMap> = mutableMapOf() - val currentOSSResultsLS: MutableMap> = mutableMapOf() + val currentSnykCodeResultsLS: MutableMap> = ConcurrentMap() + val currentOSSResultsLS: ConcurrentMap> = ConcurrentMap() - val currentContainerResultsLS: MutableMap> = mutableMapOf() + val currentContainerResultsLS: MutableMap> = ConcurrentMap() var currentContainerResult: ContainerResult? = null get() = if (field?.isExpired() == false) field else null - val currentIacResultsLS: MutableMap> = mutableMapOf() + val currentIacResultsLS: MutableMap> = ConcurrentMap() var currentIacResult: IacResult? = null get() = if (field?.isExpired() == false) field else null diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index 176b0079a..fe552c275 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -43,6 +43,7 @@ import snyk.common.annotator.SnykAnnotator.SnykAnnotation import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.RangeConverter import snyk.common.lsp.ScanIssue +import java.util.Collections import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.Icon diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 9e480358c..016060ab0 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -258,6 +258,7 @@ class LanguageServerWrapper( initializeResult = languageServer.initialize(params).get(INITIALIZATION_TIMEOUT, TimeUnit.SECONDS) languageServer.initialized(InitializedParams()) + refreshFeatureFlags() } private fun getCapabilities(): ClientCapabilities = @@ -351,6 +352,7 @@ class LanguageServerWrapper( fun refreshFeatureFlags() { runAsync { + if (!ensureLanguageServerInitialized()) return@runAsync pluginSettings().isGlobalIgnoresFeatureEnabled = isGlobalIgnoresFeatureEnabled() } } @@ -361,7 +363,7 @@ class LanguageServerWrapper( } private fun getFeatureFlagStatusInternal(featureFlag: String): Boolean { - if (pluginSettings().token.isNullOrBlank()) { + if (disposed || !isInitialized || pluginSettings().token.isNullOrBlank()) { return false } @@ -369,7 +371,7 @@ class LanguageServerWrapper( val param = ExecuteCommandParams() param.command = COMMAND_GET_FEATURE_FLAG_STATUS param.arguments = listOf(featureFlag) - val result = languageServer.workspaceService.executeCommand(param).get(20, TimeUnit.SECONDS) + val result = languageServer.workspaceService.executeCommand(param).get(5, TimeUnit.SECONDS) val resultMap = result as? Map<*, *> val ok = resultMap?.get("ok") as? Boolean ?: false diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 59202c8fa..b76bea64a 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -1,8 +1,5 @@ package snyk.common.lsp -import com.github.benmanes.caffeine.cache.Cache -import com.github.benmanes.caffeine.cache.Caffeine -import com.github.benmanes.caffeine.cache.RemovalListener import com.google.gson.Gson import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.Disposable @@ -13,9 +10,6 @@ import com.intellij.openapi.application.ReadAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectLocator import com.intellij.openapi.project.ProjectManager @@ -40,14 +34,7 @@ import org.eclipse.lsp4j.MessageType import org.eclipse.lsp4j.ProgressParams import org.eclipse.lsp4j.PublishDiagnosticsParams import org.eclipse.lsp4j.ShowMessageRequestParams -import org.eclipse.lsp4j.WorkDoneProgressBegin import org.eclipse.lsp4j.WorkDoneProgressCreateParams -import org.eclipse.lsp4j.WorkDoneProgressEnd -import org.eclipse.lsp4j.WorkDoneProgressKind.begin -import org.eclipse.lsp4j.WorkDoneProgressKind.end -import org.eclipse.lsp4j.WorkDoneProgressKind.report -import org.eclipse.lsp4j.WorkDoneProgressNotification -import org.eclipse.lsp4j.WorkDoneProgressReport import org.eclipse.lsp4j.jsonrpc.services.JsonNotification import org.eclipse.lsp4j.services.LanguageClient import org.jetbrains.concurrency.runAsync @@ -66,6 +53,7 @@ class SnykLanguageClient : Disposable { val logger = Logger.getInstance("Snyk Language Server") val gson = Gson() + private val progressManager = LSPProgressManager() private var disposed = false get() { @@ -74,25 +62,14 @@ class SnykLanguageClient : fun isDisposed() = disposed - private val progresses: Cache = - Caffeine - .newBuilder() - .expireAfterAccess(10, TimeUnit.SECONDS) - .removalListener( - RemovalListener { _, indicator, _ -> - indicator?.cancel() - }, - ).build() - private val progressReportMsgCache: Cache> = - Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build() - private val progressEndMsgCache: Cache = - Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build() - - override fun telemetryEvent(`object`: Any?) { // do nothing } + override fun notifyProgress(params: ProgressParams) { + progressManager.notifyProgress(params) + } + override fun publishDiagnostics(diagnosticsParams: PublishDiagnosticsParams?) { if (diagnosticsParams == null) { return @@ -324,135 +301,8 @@ class SnykLanguageClient : param.trustedFolders.forEach { it.toNioPathOrNull()?.let { path -> trustService.addTrustedPath(path) } } } - override fun createProgress(params: WorkDoneProgressCreateParams?): CompletableFuture = - CompletableFuture.completedFuture(null) - - private fun createProgressInternal( - token: String, - begin: WorkDoneProgressBegin, - ) { - ProgressManager - .getInstance() - .run( - object : Task.Backgroundable(ProjectUtil.getActiveProject(), "Snyk: ${begin.title}", true) { - override fun run(indicator: ProgressIndicator) { - logger.debug( - "Creating progress indicator for: $token, title: ${begin.title}, message: ${begin.message}", - ) - indicator.isIndeterminate = false - indicator.text = begin.title - indicator.text2 = begin.message - indicator.fraction = 0.1 - progresses.put(token, indicator) - while (!indicator.isCanceled && !disposed) { - Thread.sleep(1000) - } - logger.debug("Progress indicator canceled for token: $token") - } - }, - ) - } - - override fun notifyProgress(params: ProgressParams) { - if (disposed) return - // first: check if progress has begun - runAsync { - val token = params.token?.left ?: return@runAsync - if (progresses.getIfPresent(token) != null) { - processProgress(params) - } else { - when (val progressNotification = params.value.left) { - is WorkDoneProgressEnd -> { - progressEndMsgCache.put(token, progressNotification) - } - - is WorkDoneProgressReport -> { - val list = progressReportMsgCache.get(token) { mutableListOf() } - list.add(progressNotification) - } - - else -> { - processProgress(params) - } - } - return@runAsync - } - } - } - - private fun processProgress(params: ProgressParams?) { - val token = params?.token?.left ?: return - val workDoneProgressNotification = params.value.left ?: return - when (workDoneProgressNotification.kind) { - begin -> { - val begin: WorkDoneProgressBegin = workDoneProgressNotification as WorkDoneProgressBegin - createProgressInternal(token, begin) - // wait until the progress indicator is created in the background thread - while (progresses.getIfPresent(token) == null) { - Thread.sleep(100) - } - - // process previously reported progress and end messages for token - processCachedProgressReports(token) - processCachedEndReport(token) - } - - report -> { - progressReport(token, workDoneProgressNotification) - } - - end -> { - progressEnd(token, workDoneProgressNotification) - } - - null -> {} - } - } - - private fun processCachedEndReport(token: String) { - val endReport = progressEndMsgCache.getIfPresent(token) - if (endReport != null) { - progressEnd(token, endReport) - } - progressEndMsgCache.invalidate(token) - } - - private fun processCachedProgressReports(token: String) { - val reportParams = progressReportMsgCache.getIfPresent(token) - if (reportParams != null) { - reportParams.forEach { report -> - progressReport(token, report) - } - progressReportMsgCache.invalidate(token) - } - } - - private fun progressReport( - token: String, - workDoneProgressNotification: WorkDoneProgressNotification, - ) { - logger.debug("Received progress report notification for token: $token") - progresses.getIfPresent(token)?.let { - val report: WorkDoneProgressReport = workDoneProgressNotification as WorkDoneProgressReport - logger.debug("Token: $token, progress: ${report.percentage}%, message: ${report.message}") - it.text = report.message - it.isIndeterminate = false - it.fraction = report.percentage / 100.0 - } - return - } - - private fun progressEnd( - token: String, - workDoneProgressNotification: WorkDoneProgressNotification, - ) { - logger.debug("Received progress end notification for token: $token") - progresses.getIfPresent(token)?.let { - val workDoneProgressEnd = workDoneProgressNotification as WorkDoneProgressEnd - it.text = workDoneProgressEnd.message - progresses.invalidate(token) - } - return + override fun createProgress(params: WorkDoneProgressCreateParams): CompletableFuture { + return progressManager.createProgress(params) } override fun logTrace(params: LogTraceParams?) { @@ -467,6 +317,7 @@ class SnykLanguageClient : MessageType.Error -> { SnykBalloonNotificationHelper.showError(messageParams.message, project) } + MessageType.Warning -> SnykBalloonNotificationHelper.showWarn(messageParams.message, project) MessageType.Info -> { val notification = SnykBalloonNotificationHelper.showInfo(messageParams.message, project) From f7865dadaa2fa7ba5c7f5a3da28382fc91428077 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 25 Sep 2024 06:52:50 +0100 Subject: [PATCH 28/51] refactor: move files around, add progress impl from LSP4IJ --- .../SnykProjectSettingsConfigurable.kt | 2 +- .../plugin/ui/BranchChooserComboboxDialog.kt | 2 +- .../io/snyk/plugin/ui/SnykSettingsDialog.kt | 2 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 2 +- .../SnykToolWindowSnykScanListenerLS.kt | 4 - .../snyk/common/lsp/LanguageServerWrapper.kt | 2 + .../snyk/common/lsp/SnykLanguageClient.kt | 4 +- .../LSDocumentationTargetProvider.kt | 4 +- .../snyk/common/lsp/progress/Progress.kt | 45 ++++ .../common/lsp/progress/ProgressManager.kt | 220 ++++++++++++++++++ .../{ => settings}/FolderConfigSettings.kt | 3 +- .../{ => settings}/LanguageServerSettings.kt | 3 +- src/main/resources/META-INF/plugin.xml | 2 +- .../ui/BranchChooserComboBoxDialogTest.kt | 10 +- .../SnykToolWindowSnykScanListenerLSTest.kt | 14 +- .../common/lsp/LanguageServerWrapperTest.kt | 1 + 16 files changed, 298 insertions(+), 22 deletions(-) rename src/main/kotlin/snyk/common/lsp/{ => hovers}/LSDocumentationTargetProvider.kt (97%) create mode 100644 src/main/kotlin/snyk/common/lsp/progress/Progress.kt create mode 100644 src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt rename src/main/kotlin/snyk/common/lsp/{ => settings}/FolderConfigSettings.kt (97%) rename src/main/kotlin/snyk/common/lsp/{ => settings}/LanguageServerSettings.kt (98%) diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index 14520a216..e23170707 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -20,7 +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.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import javax.swing.JComponent diff --git a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt index 042d21376..961199fb1 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt @@ -9,7 +9,7 @@ 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.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import java.awt.GridBagConstraints import java.awt.GridBagLayout diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index f21c6ec99..c6d5ff450 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -48,7 +48,7 @@ import io.snyk.plugin.ui.settings.ScanTypesPanel import io.snyk.plugin.ui.settings.SeveritiesEnablementPanel import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import snyk.SnykBundle -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import java.awt.GridBagConstraints import java.awt.GridBagLayout import java.awt.Insets 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 46a828ada..9544cf91f 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -67,7 +67,7 @@ import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly import snyk.common.ProductType import snyk.common.SnykError -import snyk.common.lsp.FolderConfigSettings +import snyk.common.lsp.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue import snyk.container.ContainerIssuesForImage 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 19aba3824..4d6dfbc7e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -2,7 +2,6 @@ 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 @@ -29,13 +28,10 @@ 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.FolderConfig -import snyk.common.lsp.FolderConfigSettings import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import javax.swing.JTree diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index 016060ab0..e1b79c4cd 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -58,6 +58,8 @@ import snyk.common.lsp.commands.COMMAND_LOGOUT import snyk.common.lsp.commands.COMMAND_REPORT_ANALYTICS import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN import snyk.common.lsp.commands.ScanDoneEvent +import snyk.common.lsp.settings.LanguageServerSettings +import snyk.common.lsp.settings.SeverityFilter import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index b76bea64a..ef9863f12 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -40,6 +40,8 @@ import org.eclipse.lsp4j.services.LanguageClient import org.jetbrains.concurrency.runAsync import snyk.common.ProductType import snyk.common.editor.DocumentChanger +import snyk.common.lsp.progress.ProgressManager +import snyk.common.lsp.settings.FolderConfigSettings import snyk.trust.WorkspaceTrustService import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.CompletableFuture @@ -53,7 +55,7 @@ class SnykLanguageClient : Disposable { val logger = Logger.getInstance("Snyk Language Server") val gson = Gson() - private val progressManager = LSPProgressManager() + private val progressManager = ProgressManager() private var disposed = false get() { diff --git a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt b/src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt similarity index 97% rename from src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt rename to src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt index d19f12cb9..df8e5eccd 100644 --- a/src/main/kotlin/snyk/common/lsp/LSDocumentationTargetProvider.kt +++ b/src/main/kotlin/snyk/common/lsp/hovers/LSDocumentationTargetProvider.kt @@ -1,8 +1,7 @@ @file:Suppress("UnstableApiUsage") -package snyk.common.lsp +package snyk.common.lsp.hovers -import com.intellij.markdown.utils.convertMarkdownToHtml import com.intellij.model.Pointer import com.intellij.openapi.Disposable import com.intellij.platform.backend.documentation.DocumentationResult @@ -18,6 +17,7 @@ import org.eclipse.lsp4j.Hover import org.eclipse.lsp4j.HoverParams import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.TextDocumentIdentifier +import snyk.common.lsp.LanguageServerWrapper import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException diff --git a/src/main/kotlin/snyk/common/lsp/progress/Progress.kt b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt new file mode 100644 index 000000000..21082e48d --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt @@ -0,0 +1,45 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Copyright (c) 2024 Snyk Ltd + * + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * Snyk Ltd - adjustments for use in Snyk IntelliJ Plugin + *******************************************************************************/ +package snyk.common.lsp.progress + +import org.eclipse.lsp4j.WorkDoneProgressNotification +import java.util.concurrent.LinkedBlockingDeque +import java.util.concurrent.TimeUnit + +internal class Progress(val token: String) { + var cancellable: Boolean = false + var done: Boolean = false + + private val progressNotifications = LinkedBlockingDeque() + + var cancelled: Boolean = false + private set + + var title: String? = null + get() = if (field != null) field else token + + fun add(progressNotification: WorkDoneProgressNotification) { + progressNotifications.add(progressNotification) + } + + @get:Throws(InterruptedException::class) + val nextProgressNotification: WorkDoneProgressNotification? + get() = progressNotifications.pollFirst(200, TimeUnit.MILLISECONDS) + + fun cancel() { + this.cancelled = true + } +} diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt new file mode 100644 index 000000000..1fffb99b2 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -0,0 +1,220 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Copyright (c) 2024 Snyk Ltd + * + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + * Snyk Ltd - adjustments for use in Snyk IntelliJ Plugin + *******************************************************************************/ +package snyk.common.lsp.progress + + +import com.intellij.ide.impl.ProjectUtil +import com.intellij.openapi.Disposable +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable +import org.eclipse.lsp4j.ProgressParams +import org.eclipse.lsp4j.WorkDoneProgressBegin +import org.eclipse.lsp4j.WorkDoneProgressCancelParams +import org.eclipse.lsp4j.WorkDoneProgressCreateParams +import org.eclipse.lsp4j.WorkDoneProgressKind +import org.eclipse.lsp4j.WorkDoneProgressNotification +import org.eclipse.lsp4j.WorkDoneProgressReport +import org.eclipse.lsp4j.jsonrpc.messages.Either +import snyk.common.lsp.LanguageServerWrapper +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Function + + +class ProgressManager() : Disposable { + private val progresses: MutableMap = ConcurrentHashMap() + private var disposed = false + get() { + return SnykPluginDisposable.getInstance().isDisposed() || field + } + + fun isDisposed() = disposed + + fun createProgress(params: WorkDoneProgressCreateParams): CompletableFuture { + if (!disposed) { + val token = getToken(params.token) + getProgress(token) + } + return CompletableFuture.completedFuture(null) + } + + private fun createProgressIndicator(progress: Progress) { + val token: String = progress.token + if (isDone(progress)) { + progresses.remove(token) + return + } + val title = "Snyk: " + progress.title + val cancellable: Boolean = progress.cancellable + ProgressManager.getInstance() + .run(newProgressBackgroundTask(title, cancellable, progress, token)) + } + + private fun newProgressBackgroundTask( + title: String, + cancellable: Boolean, + progress: Progress, + token: String + ) = object : Task.Backgroundable(ProjectUtil.getActiveProject(), title, cancellable) { + override fun run(indicator: ProgressIndicator) { + try { + while (!isDone(progress)) { + if (indicator.isCanceled) { + progresses.remove(token) + val workDoneProgressCancelParams = WorkDoneProgressCancelParams() + workDoneProgressCancelParams.setToken(token) + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.isInitialized) { + val languageServer = languageServerWrapper.languageServer + languageServer.cancelProgress(workDoneProgressCancelParams) + } + throw ProcessCanceledException() + } + + var progressNotification: WorkDoneProgressNotification? + try { + progressNotification = progress.nextProgressNotification + } catch (e: InterruptedException) { + progresses.remove(token) + Thread.currentThread().interrupt() + throw ProcessCanceledException(e) + } + if (progressNotification != null) { + val kind = progressNotification.kind ?: return + when (kind) { + WorkDoneProgressKind.begin -> // 'begin' has been notified + begin(progressNotification as WorkDoneProgressBegin, indicator) + + WorkDoneProgressKind.report -> // 'report' has been notified + report(progressNotification as WorkDoneProgressReport, indicator) + + WorkDoneProgressKind.end -> Unit + } + } + } + } finally { + progresses.remove(token) + } + } + } + + private fun isDone(progress: Progress): Boolean { + return progress.done || progress.cancelled || disposed + } + + private fun begin( + begin: WorkDoneProgressBegin, + progressIndicator: ProgressIndicator + ) { + val percentage = begin.percentage + progressIndicator.isIndeterminate = percentage == null + updateProgressIndicator(begin.message, percentage, progressIndicator) + } + + private fun report( + report: WorkDoneProgressReport, + progressIndicator: ProgressIndicator + ) { + updateProgressIndicator(report.message, report.percentage, progressIndicator) + } + + @Synchronized + private fun getProgress(token: String): Progress { + var progress: Progress? = progresses[token] + if (progress != null) { + return progress + } + progress = Progress(token) + progresses[token] = progress + return progress + } + + private fun updateProgressIndicator( + message: String?, + percentage: Int?, + progressIndicator: ProgressIndicator + ) { + if (!message.isNullOrBlank()) { + progressIndicator.text = message + } + if (percentage != null) { + progressIndicator.fraction = percentage.toDouble() / 100 + } + } + + fun notifyProgress(params: ProgressParams) { + if (params.value == null || params.token == null || disposed) { + return + } + val value = params.value + if (value.isRight) { + // we don't need partial results progress support + return + } + + if (!value.isLeft) { + return + } + + val progressNotification = value.left + val kind = progressNotification.kind ?: return + val token = getToken(params.token) + var progress: Progress? = progresses[token] + if (progress == null) { + // The server is not spec-compliant and reports progress using server-initiated progress but didn't + // call window/workDoneProgress/create beforehand. In that case, we check the 'kind' field of the + // progress data. If the 'kind' field is 'begin', we set up a progress reporter anyway. + if (kind != WorkDoneProgressKind.begin) { + return + } + progress = getProgress(token) + } + + // Add the progress notification + progress.add(progressNotification) + when (progressNotification.kind!!) { + WorkDoneProgressKind.begin -> { + // 'begin' progress + val begin = progressNotification as WorkDoneProgressBegin + progress.title = begin.title + progress.cancellable = begin.cancellable != null && begin.cancellable + // The IJ task is created on 'begin' and not on 'create' to initialize + // the Task with the 'begin' title. + createProgressIndicator(progress) + } + + WorkDoneProgressKind.end -> progress.done = true + WorkDoneProgressKind.report -> Unit + } + } + + override fun dispose() { + this.disposed = true + progresses.values.forEach(Progress::cancel) + progresses.clear() + } + + companion object { + private fun getToken(token: Either): String { + return token.map( + Function.identity() + ) { obj: Int -> obj.toString() } + } + } +} diff --git a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt similarity index 97% rename from src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt rename to src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt index bd9d56e1f..77dc553af 100644 --- a/src/main/kotlin/snyk/common/lsp/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt @@ -1,4 +1,4 @@ -package snyk.common.lsp +package snyk.common.lsp.settings import com.google.gson.Gson import com.intellij.openapi.components.BaseState @@ -10,6 +10,7 @@ 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 snyk.common.lsp.FolderConfig import java.util.stream.Collectors @Service diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt similarity index 98% rename from src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt rename to src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt index 8b57c0b1d..b5c806ac1 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt @@ -1,11 +1,12 @@ @file:Suppress("unused") -package snyk.common.lsp +package snyk.common.lsp.settings 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.common.lsp.FolderConfig import snyk.pluginInfo data class LanguageServerSettings( diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 318d498f7..f500bfabf 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -55,7 +55,7 @@ id="snyk.common.codevision.LSCodeVisionProvider"/> - + diff --git a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt index 9a7b000ff..1975f7ddc 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/BranchChooserComboBoxDialogTest.kt @@ -2,9 +2,6 @@ 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 @@ -12,14 +9,13 @@ import io.mockk.mockk import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.getContentRootPaths -import io.snyk.plugin.toVirtualFile import okio.Path.Companion.toPath import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.services.LanguageServer import org.junit.Test import snyk.common.lsp.FolderConfig -import snyk.common.lsp.FolderConfigSettings -import snyk.common.lsp.LanguageServerSettings +import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LanguageServerSettings import snyk.common.lsp.LanguageServerWrapper import snyk.trust.WorkspaceTrustService import snyk.trust.WorkspaceTrustSettings @@ -31,7 +27,7 @@ class BranchChooserComboBoxDialogTest : LightPlatform4TestCase() { private lateinit var folderConfig: FolderConfig lateinit var cut: BranchChooserComboBoxDialog - override fun setUp(): Unit { + override fun setUp() { super.setUp() unmockkAll() folderConfig = FolderConfig(project.basePath.toString(), "testBranch") 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 0fa9bc0a7..7f4d5508d 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -20,9 +20,9 @@ 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 snyk.common.lsp.settings.FolderConfigSettings import snyk.trust.WorkspaceTrustSettings import java.nio.file.Paths import javax.swing.JTree @@ -123,9 +123,21 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { lesson = null, details = "", ruleId = "", + publicId = "", + documentation = "", + lineNumber = "", + issue = "", + impact = "", + resolve = "", + path = emptyList(), + references = emptyList(), + customUIContent = "", + key = "", ), isIgnored = isIgnored, ignoreDetails = null, + isNew = false, + filterableIssueType = ScanIssue.OPEN_SOURCE, ) return listOf(issue) } diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 338ae9734..cfb322366 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -29,6 +29,7 @@ import org.junit.Before import org.junit.Ignore import org.junit.Test import snyk.common.lsp.commands.ScanDoneEvent +import snyk.common.lsp.settings.FolderConfigSettings import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import java.util.concurrent.CompletableFuture From 3c250815587fcc5dfb42cba17be150b51029e3f5 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 25 Sep 2024 13:33:34 +0100 Subject: [PATCH 29/51] feat: add experimental pre-commit-hook (governed by registry setting) --- src/main/kotlin/io/snyk/plugin/Utils.kt | 4 +- .../snyk/common/PreCommitHookHandler.kt | 37 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 9 ++++- 3 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/snyk/common/PreCommitHookHandler.kt diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 92fe66bb2..40203c24e 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -61,8 +61,6 @@ import java.net.URI import java.nio.file.Path import java.util.Objects.nonNull import java.util.SortedSet -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap import java.util.concurrent.TimeUnit import javax.swing.JComponent @@ -236,6 +234,8 @@ fun isFileListenerEnabled(): Boolean = pluginSettings().fileListenerEnabled fun isDocumentationHoverEnabled(): Boolean = Registry.get("snyk.isDocumentationHoverEnabled").asBoolean() +fun isPreCommitCheckEnabled(): Boolean = Registry.get("snyk.issuesBlockCommit").asBoolean() + fun getWaitForResultsTimeout(): Long = Registry.intValue( "snyk.timeout.results.waiting", diff --git a/src/main/kotlin/snyk/common/PreCommitHookHandler.kt b/src/main/kotlin/snyk/common/PreCommitHookHandler.kt new file mode 100644 index 000000000..4004c6b0e --- /dev/null +++ b/src/main/kotlin/snyk/common/PreCommitHookHandler.kt @@ -0,0 +1,37 @@ +package snyk.common + +import com.intellij.openapi.vcs.CheckinProjectPanel +import com.intellij.openapi.vcs.changes.CommitContext +import com.intellij.openapi.vcs.checkin.CheckinHandler +import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory +import io.snyk.plugin.getSnykCachedResults +import io.snyk.plugin.isPreCommitCheckEnabled +import io.snyk.plugin.ui.SnykBalloonNotificationHelper +import snyk.common.lsp.LanguageServerWrapper + +class PreCommitHookHandler : CheckinHandlerFactory() { + override fun createHandler(panel: CheckinProjectPanel, commitContext: CommitContext): CheckinHandler { + val project = panel.project + return object : CheckinHandler() { + override fun beforeCheckin(): ReturnResult { + if (!isPreCommitCheckEnabled()) return ReturnResult.COMMIT + + val snykCachedResults = getSnykCachedResults(project) ?: return ReturnResult.COMMIT + val noCodeIssues = snykCachedResults.currentSnykCodeResultsLS.isEmpty() + val noOSSIssues = snykCachedResults.currentOSSResultsLS.isEmpty() + val noIaCIssues = snykCachedResults.currentIacResultsLS.isEmpty() + val noContainerIssues = (snykCachedResults.currentContainerResult?.issuesCount ?: 0) == 0 + LanguageServerWrapper.getInstance().sendScanCommand(project) + val doCommit = noCodeIssues && noOSSIssues && noIaCIssues && noContainerIssues + if (!doCommit) { + SnykBalloonNotificationHelper.showWarn("Stopped commit, because of you have issues.", project) + return ReturnResult.CANCEL + } + return ReturnResult.COMMIT + } + } + } + + + +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f500bfabf..3fbb9da23 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -20,6 +20,7 @@ com.intellij.modules.xml + @@ -43,6 +44,9 @@ + - + - + From 301e3c3b668f8dd54d418f75eef7a6eb9b4792ab Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 27 Sep 2024 14:42:49 +0200 Subject: [PATCH 30/51] fix: fix dispose in progress manager --- .../kotlin/snyk/common/lsp/progress/ProgressManager.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index 1fffb99b2..bd8d4da5a 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -22,6 +22,7 @@ import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.Task +import com.intellij.openapi.util.Disposer import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import org.eclipse.lsp4j.ProgressParams import org.eclipse.lsp4j.WorkDoneProgressBegin @@ -37,7 +38,7 @@ import java.util.concurrent.ConcurrentHashMap import java.util.function.Function -class ProgressManager() : Disposable { +class ProgressManager : Disposable { private val progresses: MutableMap = ConcurrentHashMap() private var disposed = false get() { @@ -46,6 +47,10 @@ class ProgressManager() : Disposable { fun isDisposed() = disposed + init { + Disposer.register(SnykPluginDisposable.getInstance(), this) + } + fun createProgress(params: WorkDoneProgressCreateParams): CompletableFuture { if (!disposed) { val token = getToken(params.token) @@ -109,6 +114,7 @@ class ProgressManager() : Disposable { } } } finally { + indicator.cancel() progresses.remove(token) } } From cb2cd06ccc419569e5ec5fa221a5aef41e744d04 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 27 Sep 2024 14:42:57 +0200 Subject: [PATCH 31/51] fix: better styling --- .../io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt | 2 +- src/main/resources/stylesheets/snyk_oss_suggestion.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 5cf05a08e..cf747bdf8 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -2,7 +2,6 @@ package io.snyk.plugin.ui.toolwindow.panels import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager -import io.snyk.plugin.ui.jcef.GenerateAIFixHandler import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager @@ -14,6 +13,7 @@ import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.baseGridConstraintsAnchorWest import io.snyk.plugin.ui.descriptionHeaderPanel import io.snyk.plugin.ui.jcef.ApplyFixHandler +import io.snyk.plugin.ui.jcef.GenerateAIFixHandler import io.snyk.plugin.ui.jcef.JCEFUtils import io.snyk.plugin.ui.jcef.LoadHandlerGenerator import io.snyk.plugin.ui.jcef.OpenFileLoadHandlerGenerator diff --git a/src/main/resources/stylesheets/snyk_oss_suggestion.scss b/src/main/resources/stylesheets/snyk_oss_suggestion.scss index 217c80c4a..810ce5ce2 100644 --- a/src/main/resources/stylesheets/snyk_oss_suggestion.scss +++ b/src/main/resources/stylesheets/snyk_oss_suggestion.scss @@ -39,7 +39,7 @@ a, } .suggestion .suggestion-text { - font-size: 1.5rem; + font-size: 1.2rem; position: relative; top: -5%; } From ae62fcc358877f30aef49054a3b8b90121b06083 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 27 Sep 2024 15:54:46 +0200 Subject: [PATCH 32/51] fix: tie progress cancellations to toolwindow nodes --- .../snyk/plugin/services/SnykTaskQueueService.kt | 14 ++++++++++---- .../kotlin/snyk/common/lsp/SnykLanguageClient.kt | 2 +- .../snyk/common/lsp/progress/ProgressManager.kt | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 807ee6839..baeaa08e1 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -1,5 +1,6 @@ package io.snyk.plugin.services +import ai.grazie.nlp.langs.Language import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger @@ -21,6 +22,8 @@ import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isCliDownloading import io.snyk.plugin.isCliInstalled +import io.snyk.plugin.isContainerRunning +import io.snyk.plugin.isIacRunning import io.snyk.plugin.isOssRunning import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.pluginSettings @@ -29,6 +32,8 @@ import io.snyk.plugin.ui.SnykBalloonNotificationHelper import org.jetbrains.annotations.TestOnly import snyk.common.SnykError import snyk.common.lsp.LanguageServerWrapper +import snyk.common.lsp.SnykLanguageClient +import snyk.common.lsp.progress.ProgressManager import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded @Service(Service.Level.PROJECT) @@ -230,13 +235,14 @@ class SnykTaskQueueService(val project: Project) { } fun stopScan() { + val languageServerWrapper = LanguageServerWrapper.getInstance() val wasOssRunning = isOssRunning(project) - cancelOssIndicator(project) - val wasSnykCodeRunning = isSnykCodeRunning(project) + val wasIacRunning = isIacRunning(project) - val wasIacRunning = iacScanProgressIndicator?.isRunning == true - iacScanProgressIndicator?.cancel() + if (languageServerWrapper.isInitialized) { + languageServerWrapper.languageClient.progressManager.cancelProgresses() + } val wasContainerRunning = containerScanProgressIndicator?.isRunning == true containerScanProgressIndicator?.cancel() diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index ef9863f12..010123fa2 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -55,7 +55,7 @@ class SnykLanguageClient : Disposable { val logger = Logger.getInstance("Snyk Language Server") val gson = Gson() - private val progressManager = ProgressManager() + val progressManager = ProgressManager() private var disposed = false get() { diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index bd8d4da5a..a1380a7b7 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -164,6 +164,12 @@ class ProgressManager : Disposable { } } + fun cancelProgresses() { + if (disposed) return + + progresses.values.forEach(Progress::cancel) + } + fun notifyProgress(params: ProgressParams) { if (params.value == null || params.token == null || disposed) { return From 92feb30b45d681522ca5365407706199e5993600 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Fri, 27 Sep 2024 15:59:25 +0200 Subject: [PATCH 33/51] fix: update isIndeterminate in progress indicator before setting progress --- src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index a1380a7b7..6a2d97b07 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -160,6 +160,7 @@ class ProgressManager : Disposable { progressIndicator.text = message } if (percentage != null) { + progressIndicator.isIndeterminate = false progressIndicator.fraction = percentage.toDouble() / 100 } } From 6fd424c34d5c1dde74ae6ce8f17cbe805f6a2f88 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Mon, 30 Sep 2024 12:23:00 +0200 Subject: [PATCH 34/51] feat: make progresses cancelable, cleanup --- src/main/kotlin/io/snyk/plugin/Utils.kt | 14 +--- .../plugin/events/SnykTaskQueueListener.kt | 7 +- .../plugin/services/SnykTaskQueueService.kt | 73 ++----------------- .../plugin/ui/toolwindow/SnykToolWindow.kt | 7 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 33 +++++---- .../SnykToolWindowSnykScanListenerLS.kt | 37 ++++------ .../common/lsp/progress/ProgressManager.kt | 69 +++++++++--------- .../services/SnykTaskQueueServiceTest.kt | 1 - ...uggestionDescriptionPanelFromLSCodeTest.kt | 2 +- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 3 +- 10 files changed, 83 insertions(+), 163 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 40203c24e..b82f2ef41 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -158,7 +158,7 @@ fun isUrlValid(url: String?): Boolean { } fun isOssRunning(project: Project): Boolean { - return isProductScanRunning(project, ProductType.OSS, getSnykTaskQueueService(project)?.ossScanProgressIndicator) + return isProductScanRunning(project, ProductType.OSS) } private fun isProductScanRunning(project: Project, productType: ProductType): Boolean { @@ -178,16 +178,6 @@ private fun isProductScanRunning( (progressIndicator != null && progressIndicator.isRunning && !progressIndicator.isCanceled) } -fun cancelOssIndicator(project: Project) { - val indicator = getSnykTaskQueueService(project)?.ossScanProgressIndicator - indicator?.cancel() -} - -fun cancelIacIndicator(project: Project) { - val indicator = getSnykTaskQueueService(project)?.iacScanProgressIndicator - indicator?.cancel() -} - fun isSnykCodeRunning(project: Project): Boolean { return isProductScanRunning(project, ProductType.CODE_SECURITY) || isProductScanRunning( project, @@ -196,7 +186,7 @@ fun isSnykCodeRunning(project: Project): Boolean { } fun isIacRunning(project: Project): Boolean { - return isProductScanRunning(project, ProductType.IAC, getSnykTaskQueueService(project)?.iacScanProgressIndicator) + return isProductScanRunning(project, ProductType.IAC) } fun isContainerRunning(project: Project): Boolean { diff --git a/src/main/kotlin/io/snyk/plugin/events/SnykTaskQueueListener.kt b/src/main/kotlin/io/snyk/plugin/events/SnykTaskQueueListener.kt index 1d1954f07..f554d082d 100644 --- a/src/main/kotlin/io/snyk/plugin/events/SnykTaskQueueListener.kt +++ b/src/main/kotlin/io/snyk/plugin/events/SnykTaskQueueListener.kt @@ -8,10 +8,5 @@ interface SnykTaskQueueListener { Topic.create("Snyk Task Queue", SnykTaskQueueListener::class.java) } - fun stopped( - wasOssRunning: Boolean = false, - wasSnykCodeRunning: Boolean = false, - wasIacRunning: Boolean = false, - wasContainerRunning: Boolean = false - ) + fun stopped() } diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index baeaa08e1..534d2b22d 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -1,6 +1,5 @@ package io.snyk.plugin.services -import ai.grazie.nlp.langs.Language import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.logger @@ -9,38 +8,29 @@ import com.intellij.openapi.progress.BackgroundTaskQueue import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project -import io.snyk.plugin.cancelOssIndicator import io.snyk.plugin.events.SnykCliDownloadListener import io.snyk.plugin.events.SnykScanListener import io.snyk.plugin.events.SnykSettingsListener import io.snyk.plugin.events.SnykTaskQueueListener import io.snyk.plugin.getContainerService -import io.snyk.plugin.getIacService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.getSnykToolWindowPanel import io.snyk.plugin.getSyncPublisher import io.snyk.plugin.isCliDownloading import io.snyk.plugin.isCliInstalled -import io.snyk.plugin.isContainerRunning -import io.snyk.plugin.isIacRunning -import io.snyk.plugin.isOssRunning -import io.snyk.plugin.isSnykCodeRunning import io.snyk.plugin.pluginSettings import io.snyk.plugin.refreshAnnotationsForOpenFiles import io.snyk.plugin.ui.SnykBalloonNotificationHelper import org.jetbrains.annotations.TestOnly -import snyk.common.SnykError import snyk.common.lsp.LanguageServerWrapper -import snyk.common.lsp.SnykLanguageClient -import snyk.common.lsp.progress.ProgressManager +import snyk.common.lsp.ScanState import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded @Service(Service.Level.PROJECT) class SnykTaskQueueService(val project: Project) { private val logger = logger() private val taskQueue = BackgroundTaskQueue(project, "Snyk") - private val taskQueueIac = BackgroundTaskQueue(project, "Snyk: Iac") private val taskQueueContainer = BackgroundTaskQueue(project, "Snyk: Container") private val settings @@ -55,12 +45,6 @@ class SnykTaskQueueService(val project: Project) { private val taskQueuePublisher get() = getSyncPublisher(project, SnykTaskQueueListener.TASK_QUEUE_TOPIC) - var ossScanProgressIndicator: ProgressIndicator? = null - private set - - var iacScanProgressIndicator: ProgressIndicator? = null - private set - var containerScanProgressIndicator: ProgressIndicator? = null private set @@ -94,7 +78,7 @@ class SnykTaskQueueService(val project: Project) { } fun scan() { - taskQueue.run(object : Task.Backgroundable(project, "Snyk: initializing...", true) { + taskQueue.run(object : Task.Backgroundable(project, "Snyk: triggering scan", true) { override fun run(indicator: ProgressIndicator) { if (!confirmScanningAndSetWorkspaceTrustedStateIfNeeded(project)) return @@ -141,7 +125,7 @@ class SnykTaskQueueService(val project: Project) { if (indicator.isCanceled) { logger.debug("cancel container scan") - taskQueuePublisher?.stopped(wasContainerRunning = true) + taskQueuePublisher?.stopped() } else { if (containerResult.isSuccessful()) { logger.debug("Container result: ->") @@ -159,49 +143,6 @@ class SnykTaskQueueService(val project: Project) { }) } - private fun scheduleIacScan() { - taskQueueIac.run(object : Task.Backgroundable(project, "Snyk Infrastructure as Code is scanning", true) { - override fun run(indicator: ProgressIndicator) { - if (!isCliInstalled()) return - val snykCachedResults = getSnykCachedResults(project) ?: return - if (snykCachedResults.currentIacResult?.iacScanNeeded == false) return - logger.debug("Starting IaC scan") - iacScanProgressIndicator = indicator - scanPublisher?.scanningStarted() - - snykCachedResults.currentIacResult = null - val iacResult = try { - getIacService(project)?.scan() - } finally { - iacScanProgressIndicator = null - } - if (iacResult == null || project.isDisposed) return - - if (indicator.isCanceled) { - logger.debug("cancel IaC scan") - taskQueuePublisher?.stopped(wasIacRunning = true) - } else { - if (iacResult.isSuccessful()) { - logger.debug("IaC result: ->") - iacResult.allCliIssues?.forEach { - logger.debug(" ${it.targetFile}, ${it.infrastructureAsCodeIssues.size} issues") - } - scanPublisher?.scanningIacFinished(iacResult) - } else { - val error = iacResult.getFirstError() - if (error == null) { - SnykError("unknown IaC error", project.basePath ?: "") - } else { - scanPublisher?.scanningIacError(error) - } - } - } - logger.debug("IaC scan completed") - refreshAnnotationsForOpenFiles(project) - } - }) - } - fun downloadLatestRelease(force: Boolean = false) { // abort even before submitting a task if (project.isDisposed || ApplicationManager.getApplication().isDisposed) return @@ -236,18 +177,14 @@ class SnykTaskQueueService(val project: Project) { fun stopScan() { val languageServerWrapper = LanguageServerWrapper.getInstance() - val wasOssRunning = isOssRunning(project) - val wasSnykCodeRunning = isSnykCodeRunning(project) - val wasIacRunning = isIacRunning(project) if (languageServerWrapper.isInitialized) { languageServerWrapper.languageClient.progressManager.cancelProgresses() + ScanState.scanInProgress.clear() } - val wasContainerRunning = containerScanProgressIndicator?.isRunning == true containerScanProgressIndicator?.cancel() - - taskQueuePublisher?.stopped(wasOssRunning, wasSnykCodeRunning, wasIacRunning, wasContainerRunning) + taskQueuePublisher?.stopped() } companion object { diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt index f9ab24016..18409b502 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt @@ -106,12 +106,7 @@ class SnykToolWindow(private val project: Project) : SimpleToolWindowPanel(false project.messageBus.connect(this) .subscribe(SnykTaskQueueListener.TASK_QUEUE_TOPIC, object : SnykTaskQueueListener { - override fun stopped( - wasOssRunning: Boolean, - wasSnykCodeRunning: Boolean, - wasIacRunning: Boolean, - wasContainerRunning: Boolean - ) = updateActionsPresentation() + override fun stopped() = updateActionsPresentation() }) } 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 9544cf91f..0daecbf71 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -269,10 +269,16 @@ class SnykToolWindowPanel( SnykResultsFilteringListener.SNYK_FILTERING_TOPIC, object : SnykResultsFilteringListener { override fun filtersChanged() { - val codeResultsLS = + val codeSecurityResultsLS = getSnykCachedResultsForProduct(project, ProductType.CODE_SECURITY) ?: return ApplicationManager.getApplication().invokeLater { - scanListenerLS.displaySnykCodeResults(codeResultsLS) + scanListenerLS.displaySnykCodeResults(codeSecurityResultsLS) + } + + val codeQualityResultsLS = + getSnykCachedResultsForProduct(project, ProductType.CODE_QUALITY) ?: return + ApplicationManager.getApplication().invokeLater { + scanListenerLS.displaySnykCodeResults(codeQualityResultsLS) } val ossResultsLS = @@ -281,6 +287,12 @@ class SnykToolWindowPanel( scanListenerLS.displayOssResults(ossResultsLS) } + val iacResultsLS = + getSnykCachedResultsForProduct(project, ProductType.IAC) ?: return + ApplicationManager.getApplication().invokeLater { + scanListenerLS.displayIacResults(iacResultsLS) + } + val snykCachedResults = getSnykCachedResults(project) ?: return ApplicationManager.getApplication().invokeLater { snykCachedResults.currentIacResult?.let { displayIacResults(it) } @@ -324,18 +336,13 @@ class SnykToolWindowPanel( .subscribe( SnykTaskQueueListener.TASK_QUEUE_TOPIC, object : SnykTaskQueueListener { - override fun stopped( - wasOssRunning: Boolean, - wasSnykCodeRunning: Boolean, - wasIacRunning: Boolean, - wasContainerRunning: Boolean, - ) = ApplicationManager.getApplication().invokeLater { + override fun stopped() = ApplicationManager.getApplication().invokeLater { updateTreeRootNodesPresentation( - ossResultsCount = if (wasOssRunning) NODE_INITIAL_STATE else null, - securityIssuesCount = if (wasSnykCodeRunning) NODE_INITIAL_STATE else null, - qualityIssuesCount = if (wasSnykCodeRunning) NODE_INITIAL_STATE else null, - iacResultsCount = if (wasIacRunning) NODE_INITIAL_STATE else null, - containerResultsCount = if (wasContainerRunning) NODE_INITIAL_STATE else null, + ossResultsCount = NODE_INITIAL_STATE, + securityIssuesCount = NODE_INITIAL_STATE, + qualityIssuesCount = NODE_INITIAL_STATE, + iacResultsCount = NODE_INITIAL_STATE, + containerResultsCount = NODE_INITIAL_STATE, ) displayEmptyDescription() } 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 4d6dfbc7e..0534239b2 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLS.kt @@ -9,8 +9,6 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.util.ui.tree.TreeUtil import io.snyk.plugin.Severity import io.snyk.plugin.SnykFile -import io.snyk.plugin.cancelIacIndicator -import io.snyk.plugin.cancelOssIndicator import io.snyk.plugin.events.SnykScanListenerLS import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.pluginSettings @@ -84,7 +82,6 @@ class SnykToolWindowSnykScanListenerLS( override fun scanningOssFinished() { if (disposed) return - cancelOssIndicator(project) ApplicationManager.getApplication().invokeLater { this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (scanning finished)" this.snykToolWindowPanel.triggerSelectionListeners = false @@ -97,7 +94,6 @@ class SnykToolWindowSnykScanListenerLS( override fun scanningIacFinished() { if (disposed) return - cancelIacIndicator(project) ApplicationManager.getApplication().invokeLater { this.rootIacIssuesTreeNode.userObject = "$IAC_ROOT_TEXT (scanning finished)" this.snykToolWindowPanel.triggerSelectionListeners = false @@ -173,36 +169,35 @@ class SnykToolWindowSnykScanListenerLS( ) } - fun displayOssResults(snykResults: Map>) { + private fun displayResults(snykResults: Map>, enabledInSettings: Boolean, filterTree: Boolean, rootNode: DefaultMutableTreeNode) { if (disposed) return - if (getSnykCachedResults(project)?.currentOssError != null) return - - val settings = pluginSettings() + if (getSnykCachedResults(project)?.currentIacError != null) return displayIssues( - enabledInSettings = settings.ossScanEnable, - filterTree = settings.treeFiltering.ossResults, + enabledInSettings = enabledInSettings, + filterTree = filterTree, snykResults = snykResults, - rootNode = this.rootOssIssuesTreeNode, - ossResultsCount = snykResults.values.flatten().distinct().size, + rootNode = rootNode, + iacResultsCount = snykResults.values.flatten().distinct().size, fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable } ) } + fun displayOssResults(snykResults: Map>) { + if (disposed) return + if (getSnykCachedResults(project)?.currentOssError != null) return + + val settings = pluginSettings() + + displayResults(snykResults, settings.ossScanEnable, settings.treeFiltering.ossResults, this.rootOssIssuesTreeNode) + } + fun displayIacResults(snykResults: Map>) { if (disposed) return if (getSnykCachedResults(project)?.currentIacError != null) return val settings = pluginSettings() - - displayIssues( - enabledInSettings = settings.iacScanEnabled, - filterTree = settings.treeFiltering.iacResults, - snykResults = snykResults, - rootNode = this.rootIacIssuesTreeNode, - iacResultsCount = snykResults.values.flatten().distinct().size, - fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable } - ) + displayResults(snykResults, settings.iacScanEnabled, settings.treeFiltering.iacResults, this.rootIacIssuesTreeNode) } private fun displayIssues( diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index 6a2d97b07..418589cee 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -76,46 +76,49 @@ class ProgressManager : Disposable { cancellable: Boolean, progress: Progress, token: String - ) = object : Task.Backgroundable(ProjectUtil.getActiveProject(), title, cancellable) { - override fun run(indicator: ProgressIndicator) { - try { - while (!isDone(progress)) { - if (indicator.isCanceled) { - progresses.remove(token) - val workDoneProgressCancelParams = WorkDoneProgressCancelParams() - workDoneProgressCancelParams.setToken(token) - val languageServerWrapper = LanguageServerWrapper.getInstance() - if (languageServerWrapper.isInitialized) { - val languageServer = languageServerWrapper.languageServer - languageServer.cancelProgress(workDoneProgressCancelParams) + ): Task.Backgroundable { + val project = ProjectUtil.getActiveProject() + return object : Task.Backgroundable(project, title, cancellable) { + override fun run(indicator: ProgressIndicator) { + try { + while (!isDone(progress)) { + if (indicator.isCanceled) { + progresses.remove(token) + val workDoneProgressCancelParams = WorkDoneProgressCancelParams() + workDoneProgressCancelParams.setToken(token) + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.isInitialized) { + val languageServer = languageServerWrapper.languageServer + languageServer.cancelProgress(workDoneProgressCancelParams) + } + throw ProcessCanceledException() } - throw ProcessCanceledException() - } - var progressNotification: WorkDoneProgressNotification? - try { - progressNotification = progress.nextProgressNotification - } catch (e: InterruptedException) { - progresses.remove(token) - Thread.currentThread().interrupt() - throw ProcessCanceledException(e) - } - if (progressNotification != null) { - val kind = progressNotification.kind ?: return - when (kind) { - WorkDoneProgressKind.begin -> // 'begin' has been notified - begin(progressNotification as WorkDoneProgressBegin, indicator) + var progressNotification: WorkDoneProgressNotification? + try { + progressNotification = progress.nextProgressNotification + } catch (e: InterruptedException) { + progresses.remove(token) + Thread.currentThread().interrupt() + throw ProcessCanceledException(e) + } + if (progressNotification != null) { + val kind = progressNotification.kind ?: return + when (kind) { + WorkDoneProgressKind.begin -> // 'begin' has been notified + begin(progressNotification as WorkDoneProgressBegin, indicator) - WorkDoneProgressKind.report -> // 'report' has been notified - report(progressNotification as WorkDoneProgressReport, indicator) + WorkDoneProgressKind.report -> // 'report' has been notified + report(progressNotification as WorkDoneProgressReport, indicator) - WorkDoneProgressKind.end -> Unit + WorkDoneProgressKind.end -> Unit + } } } + } finally { + indicator.cancel() + progresses.remove(token) } - } finally { - indicator.cancel() - progresses.remove(token) } } } diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index c4f8e706e..982947baf 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -77,7 +77,6 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() assertTrue(snykTaskQueueService.getTaskQueue().isEmpty) - assertNull(snykTaskQueueService.ossScanProgressIndicator) } fun testCliDownloadBeforeScanIfNeeded() { setupAppSettingsForDownloadTests() 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 123fbab38..4694d8510 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -62,7 +62,7 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { every { issue.cvssV3() } returns null every { issue.cvssScore() } returns null every { issue.id() } returns "id" - every { issue.additionalData.getProductType() } returns ProductType.CODE_SECURITY + every { issue.filterableIssueType } returns ScanIssue.CODE_SECURITY every { issue.additionalData.message } returns "Test message" every { issue.additionalData.repoDatasetSize } returns 1 every { issue.additionalData.exampleCommitFixes } returns 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 633f05eed..9751e5b04 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -50,7 +50,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { snykFile = SnykFile(psiFile.project, psiFile.virtualFile) val matchingIssue = mockk() - every { matchingIssue.getProductType() } returns ProductType.OSS every { matchingIssue.name } returns "Another test name" every { matchingIssue.from } returns listOf("from") every { matchingIssue.upgradePath } returns listOf("upgradePath") @@ -67,7 +66,7 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { every { issue.cvssScore() } returns "cvssScore" every { issue.id() } returns "id" every { issue.ruleId() } returns "ruleId" - every { issue.additionalData.getProductType() } returns ProductType.OSS + every { issue.filterableIssueType } returns ScanIssue.OPEN_SOURCE every { issue.additionalData.name } returns "Test name" every { issue.additionalData.matchingIssues } returns matchingIssues every { issue.additionalData.fixedIn } returns listOf("fixedIn") From 786b96890b485d30f4052f272c5ae6a5ebdb3d6e Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Tue, 1 Oct 2024 09:33:25 +0200 Subject: [PATCH 35/51] feat: transmit all progress cancellations to language server --- .../kotlin/snyk/common/lsp/progress/Progress.kt | 9 +++++++++ .../snyk/common/lsp/progress/ProgressManager.kt | 17 +++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/progress/Progress.kt b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt index 21082e48d..6da236217 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/Progress.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/Progress.kt @@ -15,7 +15,9 @@ *******************************************************************************/ package snyk.common.lsp.progress +import org.eclipse.lsp4j.WorkDoneProgressCancelParams import org.eclipse.lsp4j.WorkDoneProgressNotification +import snyk.common.lsp.LanguageServerWrapper import java.util.concurrent.LinkedBlockingDeque import java.util.concurrent.TimeUnit @@ -41,5 +43,12 @@ internal class Progress(val token: String) { fun cancel() { this.cancelled = true + val workDoneProgressCancelParams = WorkDoneProgressCancelParams() + workDoneProgressCancelParams.setToken(token) + val languageServerWrapper = LanguageServerWrapper.getInstance() + if (languageServerWrapper.isInitialized) { + val languageServer = languageServerWrapper.languageServer + languageServer.cancelProgress(workDoneProgressCancelParams) + } } } diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index 418589cee..567169db8 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -83,14 +83,7 @@ class ProgressManager : Disposable { try { while (!isDone(progress)) { if (indicator.isCanceled) { - progresses.remove(token) - val workDoneProgressCancelParams = WorkDoneProgressCancelParams() - workDoneProgressCancelParams.setToken(token) - val languageServerWrapper = LanguageServerWrapper.getInstance() - if (languageServerWrapper.isInitialized) { - val languageServer = languageServerWrapper.languageServer - languageServer.cancelProgress(workDoneProgressCancelParams) - } + cancelProgress(token) throw ProcessCanceledException() } @@ -123,6 +116,14 @@ class ProgressManager : Disposable { } } + private fun cancelProgress(token: String) { + try { + progresses[token]?.cancel() + } finally { + progresses.remove(token) + } + } + private fun isDone(progress: Progress): Boolean { return progress.done || progress.cancelled || disposed } From 6b3c340f96942bf1a51690ad62c12072e0859196 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Tue, 1 Oct 2024 09:33:41 +0200 Subject: [PATCH 36/51] chore: cleanup --- src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt index 567169db8..92f6e360e 100644 --- a/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt +++ b/src/main/kotlin/snyk/common/lsp/progress/ProgressManager.kt @@ -26,13 +26,11 @@ import com.intellij.openapi.util.Disposer import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import org.eclipse.lsp4j.ProgressParams import org.eclipse.lsp4j.WorkDoneProgressBegin -import org.eclipse.lsp4j.WorkDoneProgressCancelParams import org.eclipse.lsp4j.WorkDoneProgressCreateParams import org.eclipse.lsp4j.WorkDoneProgressKind import org.eclipse.lsp4j.WorkDoneProgressNotification import org.eclipse.lsp4j.WorkDoneProgressReport import org.eclipse.lsp4j.jsonrpc.messages.Either -import snyk.common.lsp.LanguageServerWrapper import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.function.Function From 27746b106c6b27584655cbc419a0cb9cd4ece94f Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 09:58:12 +0200 Subject: [PATCH 37/51] refactor: remove unnecessary code, centralize bulk file listening --- .../io/snyk/plugin/SnykBulkFileListener.kt | 2 +- .../io/snyk/plugin/SnykPostStartupActivity.kt | 8 +- .../snyk/plugin/SnykProjectManagerListener.kt | 1 - .../io/snyk/plugin/events/SnykScanListener.kt | 4 - src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt | 2 +- .../ui/toolwindow/SnykToolWindowPanel.kt | 88 --------- .../ui/toolwindow/SnykTreeCellRenderer.kt | 5 - .../toolwindow/panels/JCEFDescriptionPanel.kt | 100 +++++----- .../kotlin/snyk/common/SnykCachedResults.kt | 17 -- .../lsp/LanguageServerBulkFileListener.kt} | 60 ++++-- .../kotlin/snyk/iac/IacBulkFileListener.kt | 78 -------- .../snyk/iac/IacSuggestionDescriptionPanel.kt | 176 ------------------ .../snyk/iac/annotator/IacBaseAnnotator.kt | 98 ---------- .../snyk/iac/annotator/IacHclAnnotator.kt | 48 ----- .../snyk/iac/annotator/IacJsonAnnotator.kt | 35 ---- .../snyk/iac/annotator/IacYamlAnnotator.kt | 61 ------ .../snyk/iac/ui/toolwindow/IacFileTreeNode.kt | 10 - .../iac/ui/toolwindow/IacIssueTreeNode.kt | 30 --- .../kotlin/snyk/oss/OssBulkFileListener.kt | 76 -------- 19 files changed, 93 insertions(+), 806 deletions(-) rename src/main/kotlin/{io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt => snyk/common/lsp/LanguageServerBulkFileListener.kt} (77%) delete mode 100644 src/main/kotlin/snyk/iac/IacBulkFileListener.kt delete mode 100644 src/main/kotlin/snyk/iac/IacSuggestionDescriptionPanel.kt delete mode 100644 src/main/kotlin/snyk/iac/annotator/IacBaseAnnotator.kt delete mode 100644 src/main/kotlin/snyk/iac/annotator/IacHclAnnotator.kt delete mode 100644 src/main/kotlin/snyk/iac/annotator/IacJsonAnnotator.kt delete mode 100644 src/main/kotlin/snyk/iac/annotator/IacYamlAnnotator.kt delete mode 100644 src/main/kotlin/snyk/iac/ui/toolwindow/IacFileTreeNode.kt delete mode 100644 src/main/kotlin/snyk/iac/ui/toolwindow/IacIssueTreeNode.kt delete mode 100644 src/main/kotlin/snyk/oss/OssBulkFileListener.kt diff --git a/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt b/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt index 77c51d6d0..360ea2e83 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt @@ -2,6 +2,7 @@ package io.snyk.plugin import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.isFile import com.intellij.openapi.vfs.newvfs.BulkFileListener @@ -49,7 +50,6 @@ import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent * - addressed at `before` state, old file processed to _clean_ caches */ abstract class SnykBulkFileListener : BulkFileListener { - /****************************** Before **************************/ override fun before(events: List) { diff --git a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt index 810904ec5..2b7972587 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykPostStartupActivity.kt @@ -12,12 +12,10 @@ import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.vfs.VirtualFileManager import io.snyk.plugin.extensions.SnykControllerImpl import io.snyk.plugin.extensions.SnykControllerManager -import io.snyk.plugin.snykcode.SnykCodeBulkFileListener import io.snyk.plugin.ui.SnykBalloonNotifications import snyk.common.AnnotatorCommon +import snyk.common.lsp.LanguageServerBulkFileListener import snyk.container.ContainerBulkFileListener -import snyk.iac.IacBulkFileListener -import snyk.oss.OssBulkFileListener import java.time.Instant import java.time.temporal.ChronoUnit import java.util.Date @@ -43,9 +41,7 @@ class SnykPostStartupActivity : ProjectActivity { if (!listenersActivated) { val messageBusConnection = ApplicationManager.getApplication().messageBus.connect() // TODO: add subscription for language server messages - messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, OssBulkFileListener()) - messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, SnykCodeBulkFileListener()) - messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, IacBulkFileListener()) + messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, LanguageServerBulkFileListener()) messageBusConnection.subscribe(VirtualFileManager.VFS_CHANGES, ContainerBulkFileListener()) messageBusConnection.subscribe(ProjectManager.TOPIC, SnykProjectManagerListener()) listenersActivated = true diff --git a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt index 185bac6fa..862cdcbae 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykProjectManagerListener.kt @@ -22,7 +22,6 @@ class SnykProjectManagerListener : ProjectManagerListener { // limit clean up to TIMEOUT try { threadPool.submit { - // lets all running ProgressIndicators release MUTEX first val ls = LanguageServerWrapper.getInstance() if (ls.isInitialized) { ls.updateWorkspaceFolders(emptySet(), ls.getWorkspaceFolders(project)) diff --git a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt index 51c37e191..bc7fdce62 100644 --- a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt +++ b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt @@ -13,10 +13,6 @@ interface SnykScanListener { fun scanningStarted() - fun scanningIacFinished(iacResult: IacResult) - - fun scanningIacError(snykError: SnykError) - fun scanningContainerFinished(containerResult: ContainerResult) fun scanningContainerError(snykError: SnykError) diff --git a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt index 655716b01..9ba669abf 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt @@ -253,7 +253,7 @@ fun descriptionHeaderPanel( 2 + // CVSS 2 + // Snyk description customLabels.size * 2 // Labels with `|` - panel.layout = GridLayoutManager(1, columnCount, Insets(0, 0, 0, 0), 5, 0) + panel.layout = GridLayoutManager(1, columnCount, JBUI.emptyInsets(), 5, 0) panel.add( JLabel(issueNaming).apply { font = font14 }, 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 0daecbf71..3d7ac2af1 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -78,8 +78,6 @@ import snyk.container.ui.ContainerIssueTreeNode import snyk.iac.IacIssue import snyk.iac.IacResult import snyk.iac.ignorableErrorCodes -import snyk.iac.ui.toolwindow.IacFileTreeNode -import snyk.iac.ui.toolwindow.IacIssueTreeNode import java.awt.BorderLayout import java.util.Objects.nonNull import javax.swing.JPanel @@ -189,16 +187,6 @@ class SnykToolWindowPanel( } } - override fun scanningIacFinished(iacResult: IacResult) { - ApplicationManager.getApplication().invokeLater { - displayIacResults(iacResult) - } - if (iacResult.getVisibleErrors().isNotEmpty()) { - notifyAboutErrorsIfNeeded(ProductType.IAC, iacResult) - } - refreshAnnotationsForOpenFiles(project) - } - override fun scanningContainerFinished(containerResult: ContainerResult) { ApplicationManager.getApplication().invokeLater { displayContainerResults(containerResult) @@ -225,24 +213,6 @@ class SnykToolWindowPanel( } } - override fun scanningIacError(snykError: SnykError) { - var iacResultsCount: Int? = 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) - } - override fun scanningContainerError(snykError: SnykError) { var containerResultsCount: Int? = null if (snykError == ContainerService.NO_IMAGES_TO_SCAN_ERROR) { @@ -295,7 +265,6 @@ class SnykToolWindowPanel( val snykCachedResults = getSnykCachedResults(project) ?: return ApplicationManager.getApplication().invokeLater { - snykCachedResults.currentIacResult?.let { displayIacResults(it) } snykCachedResults.currentContainerResult?.let { displayContainerResults(it) } } } @@ -721,57 +690,6 @@ class SnykToolWindowPanel( revalidate() } - fun displayIacResults(iacResult: IacResult) { - val userObjectsForExpandedChildren = userObjectsForExpandedNodes(rootIacIssuesTreeNode) - val selectedNodeUserObject = TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java) - - rootIacIssuesTreeNode.removeAllChildren() - - fun navigateToIaCIssue( - virtualFile: VirtualFile?, - lineStartOffset: Int, - ): () -> Unit = - { - if (virtualFile?.isValid == true) { - navigateToSource(project, virtualFile, lineStartOffset) - } - } - - val settings = pluginSettings() - if (settings.iacScanEnabled && settings.treeFiltering.iacResults) { - iacResult.allCliIssues?.forEach { iacVulnerabilitiesForFile -> - if (iacVulnerabilitiesForFile.infrastructureAsCodeIssues.isNotEmpty()) { - val fileTreeNode = IacFileTreeNode(iacVulnerabilitiesForFile, project) - rootIacIssuesTreeNode.add(fileTreeNode) - - iacVulnerabilitiesForFile.infrastructureAsCodeIssues - .filter { settings.hasSeverityEnabledAndFiltered(it.getSeverity()) } - .sortedByDescending { it.getSeverity() } - .forEach { - val navigateToSource = - navigateToIaCIssue( - iacVulnerabilitiesForFile.virtualFile, - it.lineStartOffset, - ) - fileTreeNode.add(IacIssueTreeNode(it, project, navigateToSource)) - } - } - } - iacResult.getVisibleErrors().forEach { snykError -> - rootIacIssuesTreeNode.add( - ErrorTreeNode(snykError, project, navigateToIaCIssue(snykError.virtualFile, 0)), - ) - } - } - - updateTreeRootNodesPresentation( - iacResultsCount = iacResult.issuesCount, - addHMLPostfix = buildHMLpostfix(iacResult), - ) - - smartReloadRootNode(rootIacIssuesTreeNode, userObjectsForExpandedChildren, selectedNodeUserObject) - } - fun displayContainerResults(containerResult: ContainerResult) { val userObjectsForExpandedChildren = userObjectsForExpandedNodes(rootContainerIssuesTreeNode) val selectedNodeUserObject = TreeUtil.findObjectInPath(vulnerabilitiesTree.selectionPath, Any::class.java) @@ -949,12 +867,6 @@ class SnykToolWindowPanel( } } - fun selectNodeAndDisplayDescription(iacIssue: IacIssue) = - selectAndDisplayNodeWithIssueDescription { treeNode -> - treeNode is IacIssueTreeNode && - (treeNode.userObject as IacIssue) == iacIssue - } - fun selectNodeAndDisplayDescription(issuesForImage: ContainerIssuesForImage) = selectAndDisplayNodeWithIssueDescription { treeNode -> treeNode is ContainerImageTreeNode && 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 458d0f3a0..05d668e57 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -29,11 +29,6 @@ import snyk.container.ContainerIssue import snyk.container.ContainerIssuesForImage import snyk.container.ui.ContainerImageTreeNode import snyk.container.ui.ContainerIssueTreeNode -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile -import snyk.iac.ui.toolwindow.IacFileTreeNode -import snyk.iac.ui.toolwindow.IacIssueTreeNode -import java.util.Locale import javax.swing.Icon import javax.swing.JTree import javax.swing.tree.DefaultMutableTreeNode diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt index 1c7fc2ded..9268a1021 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/panels/JCEFDescriptionPanel.kt @@ -3,11 +3,11 @@ package io.snyk.plugin.ui.toolwindow.panels import com.intellij.openapi.editor.colors.EditorColors import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import io.snyk.plugin.SnykFile +import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.DescriptionHeaderPanel import io.snyk.plugin.ui.SnykBalloonNotificationHelper import io.snyk.plugin.ui.baseGridConstraintsAnchorWest @@ -26,7 +26,6 @@ import snyk.common.lsp.ScanIssue import stylesheets.SnykStylesheets import java.awt.BorderLayout import java.awt.Font -import java.nio.file.Paths import javax.swing.JLabel import javax.swing.JPanel import kotlin.collections.set @@ -43,65 +42,60 @@ class SuggestionDescriptionPanelFromLS( "Snyk encountered an issue while rendering the vulnerability description. Please try again, or contact support if the problem persists. We apologize for any inconvenience caused." init { - if (issue.canLoadSuggestionPanelFromHTML()) { - val loadHandlerGenerators: MutableList = - emptyList().toMutableList() + val loadHandlerGenerators: MutableList = + emptyList().toMutableList() - // TODO: replace directly in HTML instead of JS - loadHandlerGenerators += { - ThemeBasedStylingGenerator().generate(it) - } + // TODO: replace directly in HTML instead of JS + loadHandlerGenerators += { + ThemeBasedStylingGenerator().generate(it) + } - when (issue.filterableIssueType) { - ScanIssue.CODE_QUALITY, ScanIssue.CODE_SECURITY -> { - val virtualFiles = LinkedHashMap() - for (dataFlow in issue.additionalData.dataFlow) { - virtualFiles[dataFlow.filePath] = - VirtualFileManager.getInstance().findFileByNioPath(Paths.get(dataFlow.filePath)) - } + when (issue.filterableIssueType) { + ScanIssue.CODE_QUALITY, ScanIssue.CODE_SECURITY -> { + val virtualFiles = LinkedHashMap() + for (dataFlow in issue.additionalData.dataFlow) { + virtualFiles[dataFlow.filePath] = dataFlow.filePath.toVirtualFile() + } - val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile.project, virtualFiles) - loadHandlerGenerators += { - openFileLoadHandlerGenerator.generate(it) - } + val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile.project, virtualFiles) + loadHandlerGenerators += { + openFileLoadHandlerGenerator.generate(it) + } - val generateAIFixHandler = GenerateAIFixHandler() - loadHandlerGenerators += { - generateAIFixHandler.generateAIFixCommand(it) - } + val generateAIFixHandler = GenerateAIFixHandler() + loadHandlerGenerators += { + generateAIFixHandler.generateAIFixCommand(it) + } - val applyFixHandler = ApplyFixHandler(snykFile.project) - loadHandlerGenerators += { - applyFixHandler.generateApplyFixCommand(it) - } + val applyFixHandler = ApplyFixHandler(snykFile.project) + loadHandlerGenerators += { + applyFixHandler.generateApplyFixCommand(it) } } - val html = this.getCustomCssAndScript() - val jbCefBrowserComponent = - JCEFUtils.getJBCefBrowserComponentIfSupported(html, loadHandlerGenerators) - if (jbCefBrowserComponent == null) { - val statePanel = StatePanel(SnykToolWindowPanel.SELECT_ISSUE_TEXT) - this.add(wrapWithScrollPane(statePanel), BorderLayout.CENTER) - SnykBalloonNotificationHelper.showError(unexpectedErrorMessage, null) - } else { - val lastRowToAddSpacer = 5 - val panel = - JPanel( - GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20), - ).apply { - this.add( - jbCefBrowserComponent, - panelGridConstraints(1), - ) - } - this.add( - wrapWithScrollPane(panel), - BorderLayout.CENTER, - ) - this.add(panel) - } + } + val html = this.getCustomCssAndScript() + val jbCefBrowserComponent = + JCEFUtils.getJBCefBrowserComponentIfSupported(html, loadHandlerGenerators) + if (jbCefBrowserComponent == null) { + val statePanel = StatePanel(SnykToolWindowPanel.SELECT_ISSUE_TEXT) + this.add(wrapWithScrollPane(statePanel), BorderLayout.CENTER) + SnykBalloonNotificationHelper.showError(unexpectedErrorMessage, null) } else { - createUI() + val lastRowToAddSpacer = 5 + val panel = + JPanel( + GridLayoutManager(lastRowToAddSpacer + 1, 1, JBUI.insets(0, 10, 20, 10), -1, 20), + ).apply { + this.add( + jbCefBrowserComponent, + panelGridConstraints(1), + ) + } + this.add( + wrapWithScrollPane(panel), + BorderLayout.CENTER, + ) + this.add(panel) } } diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 395e63c00..308e94575 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -50,8 +50,6 @@ class SnykCachedResults( get() = if (field?.isExpired() == false) field else null val currentIacResultsLS: MutableMap> = ConcurrentMap() - var currentIacResult: IacResult? = null - get() = if (field?.isExpired() == false) field else null var currentOssError: SnykError? = null var currentContainerError: SnykError? = null @@ -60,7 +58,6 @@ class SnykCachedResults( fun cleanCaches() { currentContainerResult = null - currentIacResult = null currentOssError = null currentContainerError = null currentIacError = null @@ -83,24 +80,10 @@ class SnykCachedResults( currentContainerError = null } - override fun scanningIacFinished(iacResult: IacResult) { - currentIacResult = iacResult - } - override fun scanningContainerFinished(containerResult: ContainerResult) { currentContainerResult = containerResult } - override fun scanningIacError(snykError: SnykError) { - currentIacResult = null - currentIacError = - when { - snykError.message.startsWith(SnykToolWindowPanel.NO_IAC_FILES) -> null - snykError.message.startsWith(SnykToolWindowPanel.AUTH_FAILED_TEXT) -> null - else -> snykError - } - } - override fun scanningContainerError(snykError: SnykError) { currentContainerResult = null currentContainerError = diff --git a/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt similarity index 77% rename from src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt rename to src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt index 498a6e2af..34f0486c6 100644 --- a/src/main/kotlin/io/snyk/plugin/snykcode/SnykCodeBulkFileListener.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt @@ -1,39 +1,37 @@ -package io.snyk.plugin.snykcode +package snyk.common.lsp import com.google.common.cache.CacheBuilder import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.openapi.vfs.readText import io.snyk.plugin.SnykBulkFileListener +import io.snyk.plugin.SnykFile import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.toSnykFileSet import org.eclipse.lsp4j.DidSaveTextDocumentParams import org.eclipse.lsp4j.TextDocumentIdentifier import org.jetbrains.concurrency.runAsync -import snyk.common.lsp.LanguageServerWrapper import java.io.File import java.time.Duration -class SnykCodeBulkFileListener : SnykBulkFileListener() { - // Cache for debouncing file updates that come in within one second of the last - // Key = path, Value is irrelevant - private val debounceFileCache = - CacheBuilder.newBuilder() - .expireAfterWrite(Duration.ofMillis(1000)).build() +open class LanguageServerBulkFileListener : SnykBulkFileListener() { + override fun before( + project: Project, + virtualFilesAffected: Set, + ) = Unit - private val blackListedDirectories = - setOf(".idea", ".git", ".hg", ".svn") - - override fun before(project: Project, virtualFilesAffected: Set) = Unit - - override fun after(project: Project, virtualFilesAffected: Set) { + override fun after( + project: Project, + virtualFilesAffected: Set, + ) { if (virtualFilesAffected.isEmpty()) return runAsync { @@ -60,11 +58,19 @@ class SnykCodeBulkFileListener : SnykBulkFileListener() { ) languageServer.textDocumentService.didSave(param) } - VirtualFileManager.getInstance().asyncRefresh() - DaemonCodeAnalyzer.getInstance(project).restart() + updateCacheAndUI(filesAffected, project) } } + // Cache for debouncing file updates that come in within one second of the last + // Key = path, Value is irrelevant + private val debounceFileCache = + CacheBuilder.newBuilder() + .expireAfterWrite(Duration.ofMillis(1000)).build() + + private val blackListedDirectories = + setOf(".idea", ".git", ".hg", ".svn") + private fun shouldProcess(file: VirtualFile, index: ProjectFileIndex, project: Project): Boolean { var shouldProcess = false val application = ApplicationManager.getApplication() @@ -95,5 +101,23 @@ class SnykCodeBulkFileListener : SnykBulkFileListener() { return path.isNotEmpty() } - override fun forwardEvents(events: MutableList) = Unit + private fun updateCacheAndUI( + filesAffected: Set, + project: Project, + ) { + val ossCache = getSnykCachedResults(project)?.currentOSSResultsLS ?: return + val codeCache = getSnykCachedResults(project)?.currentSnykCodeResultsLS ?: return + val iacCache = getSnykCachedResults(project)?.currentIacResultsLS ?: return + + filesAffected.forEach { + ossCache.remove(it) + codeCache.remove(it) + iacCache.remove(it) + } + + VirtualFileManager.getInstance().asyncRefresh() + invokeLater { + DaemonCodeAnalyzer.getInstance(project).restart() + } + } } diff --git a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt b/src/main/kotlin/snyk/iac/IacBulkFileListener.kt deleted file mode 100644 index be1f02083..000000000 --- a/src/main/kotlin/snyk/iac/IacBulkFileListener.kt +++ /dev/null @@ -1,78 +0,0 @@ -package snyk.iac - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.openapi.util.io.FileUtil.pathsEqual -import com.intellij.openapi.vfs.VirtualFile -import io.snyk.plugin.SnykBulkFileListener -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.getSnykToolWindowPanel -import io.snyk.plugin.refreshAnnotationsForOpenFiles - -class IacBulkFileListener : SnykBulkFileListener() { - - private val log = logger() - - override fun before(project: Project, virtualFilesAffected: Set) { - // clean IaC cached results for deleted/moved/renamed files - updateIacCache( - virtualFilesAffected, - project - ) - } - - override fun after(project: Project, virtualFilesAffected: Set) { - // update IaC cached results if needed - updateIacCache(virtualFilesAffected, project) - } - - private fun updateIacCache( - virtualFilesAffected: Set, - project: Project - ) { - if (virtualFilesAffected.isEmpty()) return - val snykCachedResults = getSnykCachedResults(project) - val currentIacResult = snykCachedResults?.currentIacResult ?: return - val allIacIssuesForFiles = currentIacResult.allCliIssues ?: return - - val iacRelatedVFsAffected = virtualFilesAffected - .filter { scanInvalidatingFiles.contains(it.extension) } - .filter { ProjectRootManager.getInstance(project).fileIndex.isInContent(it) } - - allIacIssuesForFiles - .filter { iacIssuesForFile -> - iacRelatedVFsAffected.any { - pathsEqual(it.path, iacIssuesForFile.targetFilePath) - } - } - .forEach(::markObsolete) - - 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) - } - } - - private fun markObsolete(iacIssuesForFile: IacIssuesForFile) { - iacIssuesForFile.infrastructureAsCodeIssues.forEach { it.obsolete = true } - } - - companion object { - // see https://github.com/snyk/snyk/blob/master/src/lib/iac/constants.ts#L7 - private val scanInvalidatingFiles = listOf( - "yaml", - "yml", - "json", - "tf", - ".snyk" - ) - } -} diff --git a/src/main/kotlin/snyk/iac/IacSuggestionDescriptionPanel.kt b/src/main/kotlin/snyk/iac/IacSuggestionDescriptionPanel.kt deleted file mode 100644 index eb651d365..000000000 --- a/src/main/kotlin/snyk/iac/IacSuggestionDescriptionPanel.kt +++ /dev/null @@ -1,176 +0,0 @@ -package snyk.iac - -import com.intellij.openapi.project.Project -import com.intellij.psi.PsiFile -import com.intellij.uiDesigner.core.GridLayoutManager -import com.intellij.util.ui.UIUtil -import io.snyk.plugin.ui.DescriptionHeaderPanel -import io.snyk.plugin.ui.baseGridConstraintsAnchorWest -import io.snyk.plugin.ui.boldLabel -import io.snyk.plugin.ui.descriptionHeaderPanel -import io.snyk.plugin.ui.getFont -import io.snyk.plugin.ui.getReadOnlyClickableHtmlJEditorPane -import io.snyk.plugin.ui.insertTitleAndResizableTextIntoPanelColumns -import io.snyk.plugin.ui.panelGridConstraints -import io.snyk.plugin.ui.toolwindow.LabelProvider -import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanelBase -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer -import snyk.common.IgnoreService -import java.awt.Insets -import java.net.MalformedURLException -import java.net.URL -import javax.swing.JButton -import javax.swing.JLabel -import javax.swing.JPanel -import javax.swing.JTextArea - -class IacSuggestionDescriptionPanel( - val issue: IacIssue, - val psiFile: PsiFile?, - val project: Project -) : IssueDescriptionPanelBase(title = issue.title, severity = issue.getSeverity()) { - - private val labelProvider = LabelProvider() - - init { - this.name = "IacSuggestionDescriptionPanel" - createUI() - } - - override fun secondRowTitlePanel(): DescriptionHeaderPanel = descriptionHeaderPanel( - issueNaming = "Issue", - id = issue.id, - idUrl = issue.documentation - ) - - override fun createMainBodyPanel(): Pair { - val panel = JPanel() - val lastRowToAddSpacer = 9 - panel.layout = GridLayoutManager(lastRowToAddSpacer + 1, 1, Insets(0, 10, 20, 20), -1, 10) - - panel.add( - descriptionImpactPathPanel(), panelGridConstraints(1) - ) - - if (!issue.resolve.isNullOrBlank()) { - panel.add( - remediationPanelWithTitle(issue.resolve), panelGridConstraints(6) - ) - } - - val referencePanel = addIssueReferences() - if (referencePanel != null) { - panel.add(referencePanel, panelGridConstraints(row = 7)) - } - - return Pair(panel, lastRowToAddSpacer) - } - - private fun descriptionImpactPathPanel(): JPanel { - val mainBodyPanel = JPanel() - mainBodyPanel.layout = GridLayoutManager(11, 2, Insets(10, 0, 20, 0), 50, -1) - - insertTitleAndResizableTextIntoPanelColumns( - panel = mainBodyPanel, - row = 0, - title = "Description", - htmlText = issue.issue - ) - - insertTitleAndResizableTextIntoPanelColumns( - panel = mainBodyPanel, - row = 1, - title = "Impact", - htmlText = issue.impact - ) - - insertTitleAndResizableTextIntoPanelColumns( - panel = mainBodyPanel, - row = 2, - title = "Path", - htmlText = issue.path.joinToString(" > "), - textFont = getFont(-1, -1, JTextArea().font) ?: UIUtil.getLabelFont() - ) - - return mainBodyPanel - } - - private fun remediationPanel(resolve: String): JPanel { - val remediationPanel = JPanel() - remediationPanel.layout = GridLayoutManager(2, 1, Insets(0, 10, 20, 0), -1, -1) - remediationPanel.background = UIUtil.getTextFieldBackground() - - val resolveMarkdown = markdownToHtml(resolve) - val whiteBox = getReadOnlyClickableHtmlJEditorPane( - resolveMarkdown - ).apply { - isOpaque = false - } - - remediationPanel.add(whiteBox, panelGridConstraints(row = 1)) - - return remediationPanel - } - - private fun addIssueReferences(): JPanel? { - if (issue.references.isNotEmpty()) { - val panel = JPanel() - panel.layout = GridLayoutManager( - issue.references.size + 2, 1, Insets(20, 0, 0, 0), 50, -1 - ) - - panel.add(boldLabel("References"), baseGridConstraintsAnchorWest(row = 1)) - issue.references.forEachIndexed { index, s -> - val label = try { - labelProvider.createActionLink(URL(s), s) - } catch (e: MalformedURLException) { - JLabel(s) - } - panel.add(label, baseGridConstraintsAnchorWest(2 + index)) - } - return panel - } - return null - } - - private fun markdownToHtml(sourceStr: String): String { - val parser = Parser.builder().build() - val document = parser.parse(sourceStr) - - val renderer = HtmlRenderer.builder().escapeHtml(true).build() - - return renderer.render(document) - } - - private fun remediationPanelWithTitle(remediation: String): JPanel { - val remediationPanel = JPanel() - remediationPanel.layout = GridLayoutManager(2, 1, Insets(0, 0, 0, 0), 50, -1) - - remediationPanel.add( - boldLabel("Remediation"), - baseGridConstraintsAnchorWest(row = 0) - ) - - remediationPanel.add( - remediationPanel(remediation), - panelGridConstraints(row = 1, indent = 1) - ) - - return remediationPanel - } - - override fun getBottomRightButtons(): List = listOf( - JButton().apply { - if (issue.ignored) { - text = IgnoreButtonActionListener.IGNORED_ISSUE_BUTTON_TEXT - isEnabled = false - } else { - text = "Ignore This Issue" - addActionListener(IgnoreButtonActionListener(IgnoreService(project), issue, psiFile, project)) - } - name = "ignoreButton" - } - ) - -} diff --git a/src/main/kotlin/snyk/iac/annotator/IacBaseAnnotator.kt b/src/main/kotlin/snyk/iac/annotator/IacBaseAnnotator.kt deleted file mode 100644 index a75613843..000000000 --- a/src/main/kotlin/snyk/iac/annotator/IacBaseAnnotator.kt +++ /dev/null @@ -1,98 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.ExternalAnnotator -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.util.io.FileUtil -import com.intellij.psi.PsiFile -import io.snyk.plugin.Severity -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel -import snyk.common.AnnotatorCommon -import snyk.common.intentionactions.ShowDetailsIntentionActionBase -import snyk.iac.IacIssue -import snyk.iac.IacResult - -private val LOG = logger() - -abstract class IacBaseAnnotator : ExternalAnnotator() { - - // override needed for the Annotator to invoke apply(). We don't do anything here - override fun collectInformation(file: PsiFile): PsiFile? = file - - // save all changes on disk to update caches through SnykBulkFileListener - override fun doAnnotate(psiFile: PsiFile?) { - AnnotatorCommon.prepareAnnotate(psiFile) - } - - fun getIssues(psiFile: PsiFile): List { - val iacResult = getSnykCachedResults(psiFile.project)?.currentIacResult ?: return emptyList() - ProgressManager.checkCanceled() - return getIssuesForFile(psiFile, iacResult) - } - - override fun apply(psiFile: PsiFile, annotationResult: Unit, holder: AnnotationHolder) { - val issues = getIssues(psiFile) - .filter { AnnotatorCommon.isSeverityToShow(it.getSeverity()) } - - LOG.debug("Call apply on ${psiFile.name}") - if (issues.isEmpty()) return - - LOG.debug("Received ${issues.size} IacIssue annotations for ${psiFile.virtualFile.name}") - issues - .filter { !it.ignored && !it.obsolete } - .forEach { iacIssue -> - LOG.debug("-> ${iacIssue.id}: ${iacIssue.title}: ${iacIssue.lineNumber}") - val highlightSeverity = iacIssue.getSeverity().getHighlightSeverity() - val annotationMessage = annotationMessage(iacIssue) - holder.newAnnotation(highlightSeverity, "Snyk: $annotationMessage") - .range(textRange(psiFile, iacIssue)) - .withFix(ShowDetailsIntentionAction(annotationMessage, iacIssue)) - .create() - } - } - - abstract fun textRange(psiFile: PsiFile, iacIssue: IacIssue): TextRange - - private fun getIssuesForFile(psiFile: PsiFile, iacResult: IacResult): List { - LOG.debug("Calling getAnnotationForFile for ${psiFile.virtualFile?.path}") - - val psiFilePath = psiFile.virtualFile?.path?.let { FileUtil.toSystemIndependentName(it) } - ?: return emptyList() - - val iacIssuesForFile = iacResult.allCliIssues?.firstOrNull { iacIssuesForFile -> - val iacTargetFilePath = FileUtil.toSystemIndependentName(iacIssuesForFile.targetFilePath) - psiFilePath == iacTargetFilePath - } - LOG.debug("Found IaC issues for file: ${iacIssuesForFile?.targetFile} - ${iacIssuesForFile?.uniqueCount}") - - return iacIssuesForFile?.infrastructureAsCodeIssues?.toList() ?: emptyList() - } - - fun annotationMessage(iacIssue: IacIssue): String = iacIssue.title - - fun defaultTextRange(psiFile: PsiFile, iacIssue: IacIssue): TextRange { - val document = psiFile.viewProvider.document ?: return TextRange.EMPTY_RANGE - - val lineNumber = iacIssue.lineNumber - 1 - if (lineNumber < 0 || document.lineCount <= lineNumber) return TextRange.EMPTY_RANGE - - val startOffset = document.getLineStartOffset(lineNumber) - val endOffset = document.getLineEndOffset(lineNumber) - return TextRange.create(startOffset, endOffset) - } - - inner class ShowDetailsIntentionAction( - override val annotationMessage: String, - private val iacIssue: IacIssue - ) : ShowDetailsIntentionActionBase() { - - override fun selectNodeAndDisplayDescription(toolWindowPanel: SnykToolWindowPanel) { - toolWindowPanel.selectNodeAndDisplayDescription(iacIssue) - } - - override fun getSeverity(): Severity = iacIssue.getSeverity() - } -} diff --git a/src/main/kotlin/snyk/iac/annotator/IacHclAnnotator.kt b/src/main/kotlin/snyk/iac/annotator/IacHclAnnotator.kt deleted file mode 100644 index e0eee8879..000000000 --- a/src/main/kotlin/snyk/iac/annotator/IacHclAnnotator.kt +++ /dev/null @@ -1,48 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiWhiteSpace -import com.intellij.psi.impl.source.tree.LeafPsiElement -import com.intellij.psi.util.elementType -import org.intellij.terraform.hcl.psi.HCLBlock -import org.intellij.terraform.hcl.psi.HCLElement -import org.intellij.terraform.hcl.psi.HCLProperty -import snyk.iac.IacIssue - -private val LOG = logger() - -class IacHclAnnotator : IacBaseAnnotator() { - override fun textRange(psiFile: PsiFile, iacIssue: IacIssue): TextRange { - var textRange = defaultTextRange(psiFile, iacIssue) - - val element = getNextHCLElement(psiFile.viewProvider.findElementAt(textRange.startOffset)) - if (element != null) textRange = element.textRange - - LOG.debug("Calculated text range for ${iacIssue.id}: $textRange") - return textRange - } - - private fun getNextHCLElement(psiElement: PsiElement?): PsiElement? { - if (psiElement.elementType.toString() == "ID") { - return psiElement as LeafPsiElement - } - - return when (val nextSibling = psiElement?.nextSiblingNonWhiteSpace()) { - is HCLBlock -> nextSibling.firstChild - is HCLProperty -> nextSibling.firstChild - is HCLElement -> nextSibling.nextSibling - else -> psiElement - } - } - - private fun PsiElement.nextSiblingNonWhiteSpace(): PsiElement? { - var next = this.nextSibling - while (next != null && next is PsiWhiteSpace) { - next = next.nextSibling - } - return next - } -} diff --git a/src/main/kotlin/snyk/iac/annotator/IacJsonAnnotator.kt b/src/main/kotlin/snyk/iac/annotator/IacJsonAnnotator.kt deleted file mode 100644 index 51178dfbf..000000000 --- a/src/main/kotlin/snyk/iac/annotator/IacJsonAnnotator.kt +++ /dev/null @@ -1,35 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.json.psi.JsonElement -import com.intellij.json.psi.JsonProperty -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.util.siblings -import snyk.iac.IacIssue - -private val LOG = logger() - -class IacJsonAnnotator : IacBaseAnnotator() { - override fun textRange(psiFile: PsiFile, iacIssue: IacIssue): TextRange { - var textRange = defaultTextRange(psiFile, iacIssue) - - // try to guess precise start/end offset positions - when (val element = getNextJsonElement(psiFile.viewProvider.findElementAt(textRange.startOffset))) { - is JsonProperty -> textRange = highlightingForJsonProperty(element) - } - - LOG.debug("Calculated text range for JSON ${iacIssue.id} - $textRange") - return textRange - } - - private fun highlightingForJsonProperty(jsonProperty: JsonProperty): TextRange { - return jsonProperty.nameElement.textRange - } - - private fun getNextJsonElement(psiElement: PsiElement?): JsonElement? { - val element = psiElement?.siblings(forward = true, withSelf = false)?.firstOrNull { it is JsonElement } - return element?.let { it as JsonElement } - } -} diff --git a/src/main/kotlin/snyk/iac/annotator/IacYamlAnnotator.kt b/src/main/kotlin/snyk/iac/annotator/IacYamlAnnotator.kt deleted file mode 100644 index e3c87c403..000000000 --- a/src/main/kotlin/snyk/iac/annotator/IacYamlAnnotator.kt +++ /dev/null @@ -1,61 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.util.siblings -import com.intellij.refactoring.suggested.endOffset -import com.intellij.refactoring.suggested.startOffset -import org.jetbrains.yaml.psi.YAMLKeyValue -import org.jetbrains.yaml.psi.YAMLMapping -import org.jetbrains.yaml.psi.YAMLPsiElement -import org.jetbrains.yaml.psi.YAMLSequence -import snyk.iac.IacIssue - -private val LOG = logger() - -class IacYamlAnnotator : IacBaseAnnotator() { - override fun textRange(psiFile: PsiFile, iacIssue: IacIssue): TextRange { - var textRange = defaultTextRange(psiFile, iacIssue) - - // try to guess precise start/end offset positions - when (val element = getNextYAMLElement(psiFile.viewProvider.findElementAt(textRange.startOffset))) { - is YAMLKeyValue -> textRange = highlightingForYAMLKeyValue(element) - is YAMLMapping -> textRange = highlightingForYAMLMapping(element) - is YAMLSequence -> textRange = highlightingForYAMLSequence(element) - } - - LOG.debug("Calculated text range for ${iacIssue.id}: $textRange") - return textRange - } - - private fun highlightingForYAMLKeyValue(yamlKeyValue: YAMLKeyValue): TextRange { - val startOffset = yamlKeyValue.key?.startOffset ?: yamlKeyValue.startOffset - val endOffset = yamlKeyValue.key?.endOffset ?: yamlKeyValue.endOffset - return TextRange.create(startOffset, endOffset) - } - - private fun highlightingForYAMLMapping(mapping: YAMLMapping): TextRange { - val mappingItem = mapping.firstChild - return if (mappingItem is YAMLKeyValue) { - highlightingForYAMLKeyValue(mappingItem) - } else { - val startOffset = mapping.firstChild.startOffset - val endOffset = mapping.firstChild.endOffset - TextRange.create(startOffset, endOffset) - } - } - - private fun highlightingForYAMLSequence(yamlSequence: YAMLSequence): TextRange { - val sequenceItem = yamlSequence.items.firstOrNull() - val startOffset = sequenceItem?.keysValues?.firstOrNull()?.key?.startOffset ?: yamlSequence.startOffset - val endOffset = sequenceItem?.keysValues?.firstOrNull()?.key?.endOffset ?: yamlSequence.endOffset - return TextRange.create(startOffset, endOffset) - } - - private fun getNextYAMLElement(psiElement: PsiElement?): YAMLPsiElement? { - val element = psiElement?.siblings(forward = true, withSelf = false)?.firstOrNull { it is YAMLPsiElement } - return element?.let { it as YAMLPsiElement } - } -} diff --git a/src/main/kotlin/snyk/iac/ui/toolwindow/IacFileTreeNode.kt b/src/main/kotlin/snyk/iac/ui/toolwindow/IacFileTreeNode.kt deleted file mode 100644 index b638b7135..000000000 --- a/src/main/kotlin/snyk/iac/ui/toolwindow/IacFileTreeNode.kt +++ /dev/null @@ -1,10 +0,0 @@ -package snyk.iac.ui.toolwindow - -import com.intellij.openapi.project.Project -import snyk.iac.IacIssuesForFile -import javax.swing.tree.DefaultMutableTreeNode - -class IacFileTreeNode( - iacIssuesForFile: IacIssuesForFile, - val project: Project -) : DefaultMutableTreeNode(iacIssuesForFile) diff --git a/src/main/kotlin/snyk/iac/ui/toolwindow/IacIssueTreeNode.kt b/src/main/kotlin/snyk/iac/ui/toolwindow/IacIssueTreeNode.kt deleted file mode 100644 index 53113034a..000000000 --- a/src/main/kotlin/snyk/iac/ui/toolwindow/IacIssueTreeNode.kt +++ /dev/null @@ -1,30 +0,0 @@ -package snyk.iac.ui.toolwindow - -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFileManager -import io.snyk.plugin.findPsiFileIgnoringExceptions -import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode -import io.snyk.plugin.ui.toolwindow.nodes.NavigatableToSourceTreeNode -import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanelBase -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile -import snyk.iac.IacSuggestionDescriptionPanel -import java.nio.file.Paths -import javax.swing.tree.DefaultMutableTreeNode - -class IacIssueTreeNode( - private val issue: IacIssue, - val project: Project, - override val navigateToSource: () -> Unit, -) : DefaultMutableTreeNode(issue), NavigatableToSourceTreeNode, DescriptionHolderTreeNode { - - override fun getDescriptionPanel(): IssueDescriptionPanelBase { - val iacIssuesForFile = (this.parent as? IacFileTreeNode)?.userObject as? IacIssuesForFile - ?: throw IllegalArgumentException(this.toString()) - val fileName = iacIssuesForFile.targetFilePath - val virtualFile = VirtualFileManager.getInstance().findFileByNioPath(Paths.get(fileName)) - val psiFile = virtualFile?.let { findPsiFileIgnoringExceptions(it, project) } - - return IacSuggestionDescriptionPanel(issue, psiFile, project) - } -} diff --git a/src/main/kotlin/snyk/oss/OssBulkFileListener.kt b/src/main/kotlin/snyk/oss/OssBulkFileListener.kt deleted file mode 100644 index f6ef8d233..000000000 --- a/src/main/kotlin/snyk/oss/OssBulkFileListener.kt +++ /dev/null @@ -1,76 +0,0 @@ -package snyk.oss - -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer -import com.intellij.ide.impl.ProjectUtil -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.openapi.vfs.newvfs.events.VFileEvent -import com.intellij.openapi.vfs.readText -import io.snyk.plugin.SnykBulkFileListener -import io.snyk.plugin.SnykFile -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.toLanguageServerURL -import io.snyk.plugin.toSnykFileSet -import org.eclipse.lsp4j.DidSaveTextDocumentParams -import org.eclipse.lsp4j.TextDocumentIdentifier -import snyk.common.lsp.LanguageServerWrapper - -class OssBulkFileListener : SnykBulkFileListener() { - - override fun before( - project: Project, - virtualFilesAffected: Set, - ) = Unit - - override fun after( - project: Project, - virtualFilesAffected: Set, - ) { - val filesAffected = toSnykFileSet(project, virtualFilesAffected) - updateCacheAndUI(filesAffected, project) - } - - override fun forwardEvents(events: MutableList) { - val languageServerWrapper = LanguageServerWrapper.getInstance() - - if (!languageServerWrapper.isInitialized) return - - val languageServer = languageServerWrapper.languageServer - for (event in events) { - if (event.file == null || !event.isFromSave) continue - val file = event.file!! - val activeProject = ProjectUtil.getActiveProject() - ProgressManager.getInstance().run( - object : Task.Backgroundable( - activeProject, - "Snyk: forwarding save event to Language Server", - ) { - override fun run(indicator: ProgressIndicator) { - val param = - DidSaveTextDocumentParams( - TextDocumentIdentifier(file.toLanguageServerURL()), - file.readText(), - ) - languageServer.textDocumentService.didSave(param) - } - }, - ) - } - } - - private fun updateCacheAndUI( - filesAffected: Set, - project: Project, - ) { - val cache = getSnykCachedResults(project)?.currentOSSResultsLS ?: return - filesAffected.forEach { - cache.remove(it) - } - VirtualFileManager.getInstance().asyncRefresh() - DaemonCodeAnalyzer.getInstance(project).restart() - } -} From be2038c0a7e0bbed7abcedb91eff4c997d8ec1cc Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 09:59:45 +0200 Subject: [PATCH 38/51] chore: remove warnings --- src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt | 1 - src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt | 1 - src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt | 2 +- .../io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt | 5 +---- .../io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt | 1 - src/main/kotlin/snyk/common/SnykCachedResults.kt | 2 -- .../kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt | 1 - 7 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt b/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt index 360ea2e83..ef1298f74 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykBulkFileListener.kt @@ -2,7 +2,6 @@ package io.snyk.plugin import com.intellij.ide.impl.ProjectUtil import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.isFile import com.intellij.openapi.vfs.newvfs.BulkFileListener diff --git a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt index bc7fdce62..77afd4233 100644 --- a/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt +++ b/src/main/kotlin/io/snyk/plugin/events/SnykScanListener.kt @@ -3,7 +3,6 @@ package io.snyk.plugin.events import com.intellij.util.messages.Topic import snyk.common.SnykError import snyk.container.ContainerResult -import snyk.iac.IacResult interface SnykScanListener { companion object { diff --git a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt index 9ba669abf..656e8ca5e 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt @@ -155,7 +155,7 @@ fun getReadOnlyClickableHtmlJEditorPaneFixedSize( } fun getStandardLayout(rowCount: Int = 2, columnCount: Int = 2) = - GridLayoutManager(rowCount, columnCount, Insets(5, 5, 5, 5), -1, -1) + GridLayoutManager(rowCount, columnCount, JBUI.insets(5), -1, -1) fun getPanelWithColumns(rowCount: Int, columnCount: Int): JPanel = JPanel().apply { layout = getStandardLayout(rowCount, columnCount) } 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 3d7ac2af1..dc37fba2b 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -67,17 +67,14 @@ import io.snyk.plugin.ui.wrapWithScrollPane import org.jetbrains.annotations.TestOnly import snyk.common.ProductType import snyk.common.SnykError -import snyk.common.lsp.settings.FolderConfigSettings import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.ScanIssue +import snyk.common.lsp.settings.FolderConfigSettings import snyk.container.ContainerIssuesForImage import snyk.container.ContainerResult import snyk.container.ContainerService import snyk.container.ui.ContainerImageTreeNode import snyk.container.ui.ContainerIssueTreeNode -import snyk.iac.IacIssue -import snyk.iac.IacResult -import snyk.iac.ignorableErrorCodes import java.awt.BorderLayout import java.util.Objects.nonNull import javax.swing.JPanel 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 05d668e57..6ef5b51da 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykTreeCellRenderer.kt @@ -9,7 +9,6 @@ import io.snyk.plugin.SnykFile import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCachedResultsForProduct import io.snyk.plugin.pluginSettings -import io.snyk.plugin.ui.PackageManagerIconProvider.Companion.getIcon import io.snyk.plugin.ui.getDisabledIcon import io.snyk.plugin.ui.snykCodeAvailabilityPostfix import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 308e94575..642626549 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -19,8 +19,6 @@ import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import snyk.container.ContainerResult import snyk.container.ContainerService -import snyk.iac.IacResult -import java.util.concurrent.ConcurrentHashMap @Service(Service.Level.PROJECT) class SnykCachedResults( diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt index 34f0486c6..fee952d07 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.application.invokeLater import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.readText From e08c6f007ca25eec64c1a358a6cba21ed659da7d Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 10:03:15 +0200 Subject: [PATCH 39/51] fix: remove iac annotators from optional xml config --- src/main/resources/META-INF/optional/withHCL.xml | 1 - src/main/resources/META-INF/optional/withJSON.xml | 1 - src/main/resources/META-INF/optional/withYAML.xml | 1 - 3 files changed, 3 deletions(-) diff --git a/src/main/resources/META-INF/optional/withHCL.xml b/src/main/resources/META-INF/optional/withHCL.xml index 3ac02162d..418f6d0aa 100644 --- a/src/main/resources/META-INF/optional/withHCL.xml +++ b/src/main/resources/META-INF/optional/withHCL.xml @@ -1,5 +1,4 @@ - diff --git a/src/main/resources/META-INF/optional/withJSON.xml b/src/main/resources/META-INF/optional/withJSON.xml index 4a893eae0..418f6d0aa 100644 --- a/src/main/resources/META-INF/optional/withJSON.xml +++ b/src/main/resources/META-INF/optional/withJSON.xml @@ -1,5 +1,4 @@ - diff --git a/src/main/resources/META-INF/optional/withYAML.xml b/src/main/resources/META-INF/optional/withYAML.xml index 429ea9558..4586d10fd 100644 --- a/src/main/resources/META-INF/optional/withYAML.xml +++ b/src/main/resources/META-INF/optional/withYAML.xml @@ -1,6 +1,5 @@ - From 92bfaa301fdc09c13771c9a75107afd5a85c9aae Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 10:06:20 +0200 Subject: [PATCH 40/51] fix: unused import --- src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt index 656e8ca5e..4a2fc0672 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/UIUtils.kt @@ -21,7 +21,6 @@ import java.awt.Color import java.awt.Container import java.awt.Dimension import java.awt.Font -import java.awt.Insets import java.util.regex.Matcher import java.util.regex.Pattern import javax.swing.Icon From 0329791e8a3ec7a541335f0588a6b3711643bbfe Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 11:15:38 +0200 Subject: [PATCH 41/51] fix: fix and remove unnecessary tests --- src/main/kotlin/io/snyk/plugin/SnykFile.kt | 2 - .../lsp/LanguageServerBulkFileListener.kt | 8 +- .../analytics/AnalyticsScanListenerTest.kt | 7 - .../services/SnykTaskQueueServiceTest.kt | 21 -- .../SnykToolWindowPanelIntegTest.kt | 211 +----------------- .../SnykToolWindowSnykScanListenerLSTest.kt | 52 +---- ...uggestionDescriptionPanelFromLSCodeTest.kt | 39 ---- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 33 --- .../common/lsp/LanguageServerWrapperTest.kt | 11 +- .../snyk/iac/IacBulkFileListenerTest.kt | 199 ----------------- .../iac/IacSuggestionDescriptionPanelTest.kt | 69 ------ .../iac/IgnoreButtonActionListenerTest.kt | 76 ------- .../iac/annotator/IacBaseAnnotatorCase.kt | 38 ---- .../snyk/iac/annotator/IacHclAnnotatorTest.kt | 124 ---------- .../iac/annotator/IacJsonAnnotatorTest.kt | 74 ------ .../iac/annotator/IacYamlAnnotatorTest.kt | 99 -------- 16 files changed, 33 insertions(+), 1030 deletions(-) delete mode 100644 src/test/kotlin/snyk/iac/IacBulkFileListenerTest.kt delete mode 100644 src/test/kotlin/snyk/iac/IacSuggestionDescriptionPanelTest.kt delete mode 100644 src/test/kotlin/snyk/iac/IgnoreButtonActionListenerTest.kt delete mode 100644 src/test/kotlin/snyk/iac/annotator/IacBaseAnnotatorCase.kt delete mode 100644 src/test/kotlin/snyk/iac/annotator/IacHclAnnotatorTest.kt delete mode 100644 src/test/kotlin/snyk/iac/annotator/IacJsonAnnotatorTest.kt delete mode 100644 src/test/kotlin/snyk/iac/annotator/IacYamlAnnotatorTest.kt diff --git a/src/main/kotlin/io/snyk/plugin/SnykFile.kt b/src/main/kotlin/io/snyk/plugin/SnykFile.kt index 35ed415e5..7a2e28e46 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykFile.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykFile.kt @@ -1,11 +1,9 @@ package io.snyk.plugin import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Iconable import com.intellij.openapi.vfs.VirtualFile import org.jetbrains.concurrency.runAsync import snyk.common.RelativePathHelper -import javax.swing.Icon data class SnykFile(val project: Project, val virtualFile: VirtualFile) { val relativePath = runAsync { RelativePathHelper().getRelativePath(virtualFile, project) } diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt index fee952d07..7b44d7cf2 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerBulkFileListener.kt @@ -17,11 +17,16 @@ import io.snyk.plugin.toLanguageServerURL import io.snyk.plugin.toSnykFileSet import org.eclipse.lsp4j.DidSaveTextDocumentParams import org.eclipse.lsp4j.TextDocumentIdentifier +import org.jetbrains.annotations.TestOnly import org.jetbrains.concurrency.runAsync +import org.jetbrains.kotlin.idea.util.application.isUnitTestMode import java.io.File import java.time.Duration -open class LanguageServerBulkFileListener : SnykBulkFileListener() { +class LanguageServerBulkFileListener : SnykBulkFileListener() { + @TestOnly + var disabled = isUnitTestMode() + override fun before( project: Project, virtualFilesAffected: Set, @@ -31,6 +36,7 @@ open class LanguageServerBulkFileListener : SnykBulkFileListener() { project: Project, virtualFilesAffected: Set, ) { + if (disabled) return if (virtualFilesAffected.isEmpty()) return runAsync { diff --git a/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt index 883e74831..9949883c3 100644 --- a/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/analytics/AnalyticsScanListenerTest.kt @@ -100,13 +100,6 @@ class AnalyticsScanListenerTest { assertNotNull(scanDoneEvent.data.attributes.timestampFinished) } - @Test - fun `testScanListener scanningIacFinished should call language server to report analytics`() { - cut.snykScanListener.scanningIacFinished(mockk(relaxed = true)) - - verify { languageServerWrapper.sendReportAnalyticsCommand(any()) } - } - @Test fun `testScanListener scanningContainerFinished should call language server to report analytics`() { cut.snykScanListener.scanningContainerFinished(mockk(relaxed = true)) diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index 982947baf..a09ca0166 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -139,27 +139,6 @@ class SnykTaskQueueServiceTest : LightPlatformTestCase() { assertNull(settings.localCodeEngineEnabled) } - fun testIacScanTriggeredAndProduceResults() { - val snykTaskQueueService = project.service() - val settings = pluginSettings() - settings.ossScanEnable = false - settings.snykCodeSecurityIssuesScanEnable = false - settings.snykCodeQualityIssuesScanEnable = false - settings.iacScanEnabled = true - getSnykCachedResults(project)?.currentIacResult = null - - val fakeIacResult = IacResult(emptyList()) - - mockkStatic("io.snyk.plugin.UtilsKt") - every { isCliInstalled() } returns true - every { getIacService(project)?.scan() } returns fakeIacResult - - snykTaskQueueService.scan() - PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue() - - assertEquals(fakeIacResult, getSnykCachedResults(project)?.currentIacResult) - } - fun testContainerScanTriggeredAndProduceResults() { mockkStatic("io.snyk.plugin.UtilsKt") every { isCliInstalled() } returns true 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 421f22ea0..3b9b8f53f 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt @@ -58,10 +58,7 @@ import snyk.iac.IacIssue import snyk.iac.IacIssuesForFile import snyk.iac.IacResult import snyk.iac.IacScanService -import snyk.iac.IacSuggestionDescriptionPanel import snyk.iac.IgnoreButtonActionListener -import snyk.iac.ui.toolwindow.IacFileTreeNode -import snyk.iac.ui.toolwindow.IacIssueTreeNode import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.CompletableFuture import javax.swing.JButton @@ -150,46 +147,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { } } - private fun prepareTreeWithFakeOssResults() { - val param = - ExecuteCommandParams(COMMAND_EXECUTE_CLI, listOf(project.basePath, "test", "--json", "--all-projects")) - - every { lsMock.workspaceService.executeCommand(param) } returns - CompletableFuture.completedFuture(mapOf(Pair("stdOut", ossGoofJson))) - - LanguageServerWrapper.getInstance().sendScanCommand(project) - - PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) - } - - private fun prepareTreeWithFakeIacResults() { - setUpIacTest() - - val lsMock = mockk() - LanguageServerWrapper.getInstance().languageServer = lsMock - val param = ExecuteCommandParams(COMMAND_EXECUTE_CLI, listOf(project.basePath, "iac", "test", "--json")) - - every { lsMock.workspaceService.executeCommand(param) } returns - CompletableFuture.completedFuture(mapOf(Pair("stdOut", iacGoofJson))) - - val ignoreParam = ExecuteCommandParams( - COMMAND_EXECUTE_CLI, - listOf( - project.basePath, - "ignore", - "--id=SNYK-CC-TF-53", - "--path=* > [DocId:0] > Resources > LaunchConfig > Properties > BlockDeviceMappings" - ) - ) - - every { lsMock.workspaceService.executeCommand(ignoreParam) } returns - CompletableFuture.completedFuture(mapOf(Pair("stdOut", ""))) - - val iacResult = project.service().scan() - scanPublisher.scanningIacFinished(iacResult) - PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) - } - private val fakeContainerIssue1 = ContainerIssue( id = "fakeId1", title = "fakeTitle1", @@ -289,28 +246,27 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { mockkObject(SnykBalloonNotificationHelper) val snykError = - SnykError(SnykToolWindowPanel.NO_IAC_FILES, project.basePath.toString(), IacError.NO_IAC_FILES_CODE) + SnykScanParams("failed", "iac", project.basePath!!, emptyList(), SnykToolWindowPanel.NO_IAC_FILES) + val snykErrorControl = SnykScanParams("failed", "iac", project.basePath!!, emptyList(), "control") - val snykErrorControl = SnykError("control", project.basePath.toString()) - - scanPublisher.scanningIacError(snykErrorControl) - scanPublisher.scanningIacError(snykError) + scanPublisherLS.scanningError(snykErrorControl) + scanPublisherLS.scanningError(snykError) PlatformTestUtil.dispatchAllEventsInIdeEventQueue() + val rootIacTreeNode = toolWindowPanel.getRootIacIssuesTreeNode() // flow and internal state check verify(exactly = 1, timeout = 2000) { - SnykBalloonNotificationHelper.showError(snykErrorControl.message, project) + SnykBalloonNotificationHelper.showError(any(), project) } - assertTrue(getSnykCachedResults(project)?.currentIacError == null) - assertTrue(getSnykCachedResults(project)?.currentIacResult == null) + assertTrue(getSnykCachedResults(project)?.currentIacResultsLS?.isEmpty() ?: false) // node check assertEquals( - SnykToolWindowPanel.IAC_ROOT_TEXT + SnykToolWindowPanel.NO_SUPPORTED_IAC_FILES_FOUND, - toolWindowPanel.getRootIacIssuesTreeNode().userObject + SnykToolWindowPanel.OSS_ROOT_TEXT + SnykToolWindowPanel.NO_SUPPORTED_PACKAGE_MANAGER_FOUND, + rootIacTreeNode.userObject ) // description check - TreeUtil.selectNode(toolWindowPanel.getTree(), toolWindowPanel.getRootIacIssuesTreeNode()) + TreeUtil.selectNode(toolWindowPanel.getTree(), rootIacTreeNode) PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) val jEditorPane = UIComponentFinder.getComponentByName( toolWindowPanel.getDescriptionPanel(), @@ -318,44 +274,7 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { ) assertNotNull(jEditorPane) jEditorPane!! - assertTrue(jEditorPane.text.contains(SnykToolWindowPanel.NO_IAC_FILES)) - } - - fun `test should ignore IaC failures in IaC scan results (no issues found)`() { - mockkObject(SnykBalloonNotificationHelper) - val jsonError = SnykError("Failed to parse JSON file", project.basePath.toString(), 1021) - val inputError = SnykError("Failed to parse input", project.basePath.toString(), 2105) - val iacResult = IacResult(emptyList(), listOf(jsonError, inputError)) - scanPublisher.scanningIacFinished(iacResult) - PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertEquals( - SnykToolWindowPanel.IAC_ROOT_TEXT + SnykToolWindowPanel.NO_ISSUES_FOUND_TEXT, - toolWindowPanel.getRootIacIssuesTreeNode().userObject - ) - } - - fun `test should ignore IaC failures in IaC scan results (issues found)`() { - mockkObject(SnykBalloonNotificationHelper) - val iacIssue = IacIssue( - id = "SNYK-CC-TF-74", - title = "Credentials are configured via provider attributes", - lineNumber = 1, - severity = "", - publicId = "", - documentation = "", - issue = "", - impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), "k8s-deployment.yaml", "src/k8s-deployment.yaml", "npm", null, project) - val jsonError = SnykError("Failed to parse JSON file", project.basePath.toString(), 1021) - val iacResult = IacResult(listOf(iacIssuesForFile), listOf(jsonError)) - scanPublisher.scanningIacFinished(iacResult) - PlatformTestUtil.dispatchAllEventsInIdeEventQueue() - assertEquals( - SnykToolWindowPanel.IAC_ROOT_TEXT + " - 1 unique issue", - toolWindowPanel.getRootIacIssuesTreeNode().userObject - ) + assertTrue(jEditorPane.text.contains(SnykToolWindowPanel.NO_OSS_FILES)) } fun `test should display NO_CONTAINER_IMAGES_FOUND after scan when no Container images found`() { @@ -545,32 +464,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { ) } - fun testSeverityFilterForIacResult() { - // pre-test setup - prepareTreeWithFakeIacResults() - - // actual test run - val rootIacIssuesTreeNode = toolWindowPanel.getRootIacIssuesTreeNode() - fun isMediumSeverityShown(): Boolean = rootIacIssuesTreeNode.children().asSequence() - .flatMap { (it as TreeNode).children().asSequence() } - .any { - it is IacIssueTreeNode && - it.userObject is IacIssue && - (it.userObject as IacIssue).getSeverity() == Severity.MEDIUM - } - - assertTrue("Medium severity IaC results should be shown by default", isMediumSeverityShown()) - - val mediumSeverityFilterAction = - ActionManager.getInstance().getAction("io.snyk.plugin.ui.actions.SnykTreeMediumSeverityFilterAction") - as SnykTreeMediumSeverityFilterAction - mediumSeverityFilterAction.setSelected(TestActionEvent.createTestEvent(), false) - - PlatformTestUtil.waitWhileBusy(toolWindowPanel.getTree()) - - assertFalse("Medium severity IaC results should NOT be shown after filtering", isMediumSeverityShown()) - } - fun testIacErrorShown() { // pre-test setup setUpIacTest() @@ -605,61 +498,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { assertTrue(pathTextArea?.text == iacError.path) } - fun test_WhenIacIssueIgnored_ThenItMarkedIgnored_AndButtonRemainsDisabled() { - // pre-test setup - prepareTreeWithFakeIacResults() - - val tree = toolWindowPanel.getTree() - - // select first IaC issue and ignore it - val rootIacIssuesTreeNode = toolWindowPanel.getRootIacIssuesTreeNode() - val firstIaCFileNode = rootIacIssuesTreeNode.firstChild as? IacFileTreeNode - val firstIacIssueNode = firstIaCFileNode?.firstChild as? IacIssueTreeNode - ?: throw IllegalStateException("IacIssueNode should not be null") - TreeUtil.selectNode(tree, firstIacIssueNode) - waitWhileTreeBusy() - - fun iacDescriptionPanel() = - UIComponentFinder.getComponentByName( - toolWindowPanel.getDescriptionPanel(), - IacSuggestionDescriptionPanel::class, - "IacSuggestionDescriptionPanel" - ) ?: throw IllegalStateException("IacSuggestionDescriptionPanel should not be null") - - val ignoreButton = UIComponentFinder.getComponentByName(iacDescriptionPanel(), JButton::class, "ignoreButton") - ?: throw IllegalStateException("IgnoreButton should not be null") - - assertFalse( - "Issue should NOT be ignored by default", - (firstIacIssueNode.userObject as IacIssue).ignored - ) - assertTrue( - "Ignore Button should be enabled by default", - ignoreButton.isEnabled && ignoreButton.text != IgnoreButtonActionListener.IGNORED_ISSUE_BUTTON_TEXT - ) - - ignoreButton.doClick() - - // check final state - assertTrue( - "Issue should be marked as ignored after ignoring", - (firstIacIssueNode.userObject as IacIssue).ignored - ) - assertTrue( - "Ignore Button should be disabled for ignored issue", - !ignoreButton.isEnabled && ignoreButton.text == IgnoreButtonActionListener.IGNORED_ISSUE_BUTTON_TEXT - ) - PlatformTestUtil.waitWhileBusy(tree) - TreeUtil.selectNode(tree, firstIacIssueNode.nextNode) - PlatformTestUtil.waitWhileBusy(tree) - TreeUtil.selectNode(tree, firstIacIssueNode) - PlatformTestUtil.waitWhileBusy(tree) - assertTrue( - "Ignore Button should remain disabled for ignored issue", - !ignoreButton.isEnabled && ignoreButton.text == IgnoreButtonActionListener.IGNORED_ISSUE_BUTTON_TEXT - ) - } - fun `test all root nodes are shown`() { setUpIacTest() setUpContainerTest(null) @@ -899,33 +737,6 @@ class SnykToolWindowPanelIntegTest : HeavyPlatformTestCase() { PlatformTestUtil.waitWhileBusy(tree) } - fun `test IaC node selected and Description shown on external request`() { - // pre-test setup - prepareTreeWithFakeIacResults() - - val rootIacIssuesTreeNode = toolWindowPanel.getRootIacIssuesTreeNode() - val firstIaCFileNode = rootIacIssuesTreeNode.firstChild as? IacFileTreeNode - val firstIacIssueNode = firstIaCFileNode?.firstChild as? IacIssueTreeNode - ?: throw IllegalStateException("IacIssueNode should not be null") - val iacIssue = firstIacIssueNode.userObject as IacIssue - - // actual test run - toolWindowPanel.selectNodeAndDisplayDescription(iacIssue) - waitWhileTreeBusy() - - // Assertions - val selectedNodeUserObject = TreeUtil.findObjectInPath(toolWindowPanel.getTree().selectionPath, Any::class.java) - assertEquals(iacIssue, selectedNodeUserObject) - - val iacDescriptionPanel = - UIComponentFinder.getComponentByName( - toolWindowPanel.getDescriptionPanel(), - IacSuggestionDescriptionPanel::class, - "IacSuggestionDescriptionPanel" - ) - assertNotNull("IacSuggestionDescriptionPanel should not be null", iacDescriptionPanel) - } - fun `test Container node selected and Description shown on external request`() { // prepare Tree with fake Container results setUpContainerTest(null) 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 7f4d5508d..4465d14af 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerLSTest.kt @@ -11,6 +11,7 @@ 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.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 @@ -67,6 +68,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootOssIssuesTreeNode = RootOssTreeNode(project) rootSecurityIssuesTreeNode = RootSecurityIssuesTreeNode(project) rootQualityIssuesTreeNode = RootQualityIssuesTreeNode(project) + rootIacIssuesTreeNode = RootIacIssuesTreeNode(project) pluginSettings().setDeltaEnabled() contentRootPaths.forEach { service().addTrustedPath(it.root.absolutePathString())} } @@ -150,6 +152,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) + rootTreeNode.add(rootIacIssuesTreeNode) vulnerabilitiesTree = Tree(rootTreeNode).apply { this.isRootVisible = false } @@ -164,7 +167,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootIacIssuesTreeNode ) - TestCase.assertEquals(3, rootTreeNode.childCount) + TestCase.assertEquals(4, rootTreeNode.childCount) cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(), 1) TestCase.assertEquals(6, rootTreeNode.childCount) TestCase.assertEquals(rootTreeNode.children().toList()[0].toString(), " Open Source") @@ -180,47 +183,6 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { ) } - 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, - rootIacIssuesTreeNode - ) - - 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.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(), - ) - TestCase.assertEquals( - "⚡ 1 vulnerabilities can be fixed automatically", - rootTreeNode.children().toList()[5].toString(), - ) - } - fun `testAddInfoTreeNodes adds new tree nodes for code security if all ignored issues are hidden`() { pluginSettings().isGlobalIgnoresFeatureEnabled = true pluginSettings().ignoredIssuesEnabled = false @@ -230,6 +192,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) + rootTreeNode.add(rootIacIssuesTreeNode) vulnerabilitiesTree = Tree(rootTreeNode).apply { this.isRootVisible = false } @@ -244,7 +207,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootIacIssuesTreeNode ) - TestCase.assertEquals(3, rootTreeNode.childCount) + TestCase.assertEquals(4, rootTreeNode.childCount) cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(true), 1) TestCase.assertEquals(7, rootTreeNode.childCount) } @@ -258,6 +221,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootTreeNode.add(rootOssIssuesTreeNode) rootTreeNode.add(rootSecurityIssuesTreeNode) rootTreeNode.add(rootQualityIssuesTreeNode) + rootTreeNode.add(rootIacIssuesTreeNode) vulnerabilitiesTree = Tree(rootTreeNode).apply { this.isRootVisible = false } @@ -272,7 +236,7 @@ class SnykToolWindowSnykScanListenerLSTest : BasePlatformTestCase() { rootIacIssuesTreeNode ) - TestCase.assertEquals(3, rootTreeNode.childCount) + TestCase.assertEquals(4, rootTreeNode.childCount) cut.addInfoTreeNodes(rootTreeNode, mockScanIssues(false), 1) TestCase.assertEquals(7, rootTreeNode.childCount) } diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt index 4694d8510..c16e5a30c 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -79,45 +79,6 @@ class SuggestionDescriptionPanelFromLSCodeTest : BasePlatformTestCase() { } returns listOf(DataFlow(0, getTestDataPath(), Range(Position(1, 1), Position(1, 1)), "")) } - fun `test createUI should build the right panels for Snyk Code if HTML is not allowed`() { - every { issue.canLoadSuggestionPanelFromHTML() } returns false - - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) - - val issueNaming = getJLabelByText(cut, issue.issueNaming()) - assertNotNull(issueNaming) - - val overviewPanel = getJLabelByText(cut, "Test message") - assertNotNull(overviewPanel) - - val dataFlowPanel = getJPanelByName(cut, "dataFlowPanel") - assertNotNull(dataFlowPanel) - - val fixExamplesPanel = getJPanelByName(cut, "fixExamplesPanel") - assertNotNull(fixExamplesPanel) - - val introducedThroughPanel = getJPanelByName(cut, "introducedThroughPanel") - assertNull(introducedThroughPanel) - - val detailedPathsPanel = getJPanelByName(cut, "detailedPathsPanel") - assertNull(detailedPathsPanel) - - val ossOverviewPanel = getJPanelByName(cut, "overviewPanel") - assertNull(ossOverviewPanel) - } - - fun `test createUI should build panel with issue message as overview label if HTML is not allowed`() { - every { issue.canLoadSuggestionPanelFromHTML() } returns false - - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) - - val actual = getJLabelByText(cut, "Test message") - assertNotNull(actual) - - val actualBrowser = getJBCEFBrowser(cut) - assertNull(actualBrowser) - } - 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 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 9751e5b04..3cc4f6d25 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -77,39 +77,6 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { } returns emptyList() } - fun `test createUI should build the right panels for Snyk OSS if HTML not allowed`() { - every { issue.canLoadSuggestionPanelFromHTML() } returns false - - cut = SuggestionDescriptionPanelFromLS(snykFile, issue) - - val issueNaming = getJLabelByText(cut, issue.issueNaming()) - assertNotNull(issueNaming) - - val cvssScore = getActionLinkByText(cut, "CVSS cvssScore") - assertNotNull(cvssScore) - - val ruleId = getActionLinkByText(cut, "ID") - assertNotNull(ruleId) - - val overviewPanel = getJLabelByText(cut, "Test message") - assertNull(overviewPanel) - - val dataFlowPanel = getJPanelByName(cut, "dataFlowPanel") - assertNull(dataFlowPanel) - - val fixExamplesPanel = getJPanelByName(cut, "fixExamplesPanel") - assertNull(fixExamplesPanel) - - val introducedThroughPanel = getJPanelByName(cut, "introducedThroughPanel") - assertNotNull(introducedThroughPanel) - - val detailedPathsPanel = getJPanelByName(cut, "detailedPathsPanel") - assertNotNull(detailedPathsPanel) - - val ossOverviewPanel = getJPanelByName(cut, "overviewPanel") - assertNotNull(ossOverviewPanel) - } - fun `test createUI should build panel with HTML from details if allowed`() { val mockJBCefBrowserComponent = JLabel("HTML message") mockkObject(JCEFUtils) diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index cfb322366..3c78c5a4b 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -2,6 +2,7 @@ package snyk.common.lsp import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.impl.AnyModalityState import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager @@ -36,7 +37,7 @@ import java.util.concurrent.CompletableFuture class LanguageServerWrapperTest { private val folderConfigSettingsMock: FolderConfigSettings = mockk(relaxed = true) - private val applicationMock: Application = mockk() + private val applicationMock: Application = mockk(relaxed = true) private val projectMock: Project = mockk() private val lsMock: LanguageServer = mockk() private val settings = SnykApplicationSettingsStateService() @@ -319,13 +320,15 @@ class LanguageServerWrapperTest { val actual = cut.getSettings() - assertEquals("false", actual.activateSnykCode) - assertEquals("false", actual.activateSnykIac) - assertEquals("true", actual.activateSnykOpenSource) + assertEquals(settings.snykCodeQualityIssuesScanEnable.toString(), actual.activateSnykCodeQuality) + assertEquals(settings.snykCodeSecurityIssuesScanEnable.toString(), actual.activateSnykCodeSecurity) + assertEquals(settings.iacScanEnabled.toString(), actual.activateSnykIac) + assertEquals(settings.ossScanEnable.toString(), actual.activateSnykOpenSource) assertEquals(settings.token, actual.token) assertEquals("${settings.ignoreUnknownCA}", actual.insecure) assertEquals(getCliFile().absolutePath, actual.cliPath) assertEquals(settings.organization, actual.organization) + assertEquals(settings.isDeltaFindingsEnabled().toString(), actual.enableDeltaFindings) } @Ignore // somehow it doesn't work in the pipeline diff --git a/src/test/kotlin/snyk/iac/IacBulkFileListenerTest.kt b/src/test/kotlin/snyk/iac/IacBulkFileListenerTest.kt deleted file mode 100644 index 09b1ac3b3..000000000 --- a/src/test/kotlin/snyk/iac/IacBulkFileListenerTest.kt +++ /dev/null @@ -1,199 +0,0 @@ -package snyk.iac - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.service -import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.roots.ProjectRootManager -import com.intellij.psi.PsiDocumentManager -import com.intellij.testFramework.PsiTestUtil -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import io.mockk.every -import io.mockk.mockk -import io.snyk.plugin.getSnykCachedResults -import io.snyk.plugin.resetSettings -import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel -import org.awaitility.Awaitility.await -import org.eclipse.lsp4j.services.LanguageServer -import org.junit.Test -import snyk.common.lsp.LanguageServerWrapper -import snyk.iac.ui.toolwindow.IacFileTreeNode -import java.util.concurrent.TimeUnit - -class IacBulkFileListenerTest : BasePlatformTestCase() { - private val lsMock = mockk(relaxed = true) - - override fun setUp() { - super.setUp() - resetSettings(project) - val languageServerWrapper = LanguageServerWrapper.getInstance() - languageServerWrapper.isInitialized = true - languageServerWrapper.languageServer = lsMock - } - - override fun tearDown() { - resetSettings(project) - try { - super.tearDown() - } catch (ignore: Exception) { - // nothing to do as we're shutting down the test - } - } - - /** `filePath == null` is the case when we want to check if _any_ IaC file with issues been marked as obsolete */ - private fun iacCacheInvalidatedForFilePath(filePath: String?): Boolean { - val iacCachedIssues = getSnykCachedResults(project)?.currentIacResult!!.allCliIssues!! - return iacCachedIssues.any { iacFile -> - (filePath == null || iacFile.targetFilePath == filePath) && iacFile.obsolete - } - } - - private fun isIacUpdateNeeded(): Boolean = - getSnykCachedResults(project)?.currentIacResult?.iacScanNeeded ?: true - - private fun createFakeIacResultInCache(file: String, filePath: String) { - val iacIssue = IacIssue( - id = "SNYK-CC-TF-74", - title = "Credentials are configured via provider attributes", - lineNumber = 1, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = IacIssuesForFile(listOf(iacIssue), file, filePath, "npm", null, project) - val iacVulnerabilities = listOf(iacIssuesForFile) - val fakeIacResult = IacResult(iacVulnerabilities) - getSnykCachedResults(project)?.currentIacResult = fakeIacResult - val rootIacIssuesTreeNode = project.service().getRootIacIssuesTreeNode() - rootIacIssuesTreeNode.add(IacFileTreeNode(iacIssuesForFile, project)) - } - - @Test - fun `test currentIacResults should be dropped when IaC supported file changed`() { - val file = "k8s-deployment.yaml" - val filePath = "/src/$file" - createFakeIacResultInCache(file, filePath) - - myFixture.configureByText(file, "some text") - - await().atMost(2, TimeUnit.SECONDS).until { iacCacheInvalidatedForFilePath(filePath) } - } - - @Test - fun `test current IacResults should mark IacScanNeeded when IaC supported file CREATED`() { - val existingFile = "existing.yaml" - createFakeIacResultInCache(existingFile, "/src/$existingFile") - - assertFalse(isIacUpdateNeeded()) - - myFixture.addFileToProject("new.yaml", "some text") - - assertTrue(isIacUpdateNeeded()) - assertFalse( - "None of IaC file with issues should been marked as obsolete here", - iacCacheInvalidatedForFilePath(null) - ) - } - - @Test - fun `test current IacResults should mark IacScanNeeded when IaC supported file COPIED`() { - val originalFileName = "existing.yaml" - val originalFile = myFixture.addFileToProject(originalFileName, "some text") - createFakeIacResultInCache(originalFileName, originalFile.virtualFile.path) - - assertFalse(isIacUpdateNeeded()) - - ApplicationManager.getApplication().runWriteAction { - originalFile.virtualFile.copy(null, originalFile.virtualFile.parent, "copied.yaml") - } - - assertTrue(isIacUpdateNeeded()) - assertFalse( - "None of IaC file with issues should been marked as obsolete here", - iacCacheInvalidatedForFilePath(null) - ) - } - - @Test - fun `test current IacResults should drop cache and mark IacScanNeeded when IaC supported file MOVED`() { - val originalFileName = "existing.yaml" - val originalFile = myFixture.addFileToProject(originalFileName, "some text") - val originalFilePath = originalFile.virtualFile.path - createFakeIacResultInCache(originalFileName, originalFilePath) - - assertFalse(isIacUpdateNeeded()) - - ApplicationManager.getApplication().runWriteAction { - val moveToDirVirtualFile = originalFile.virtualFile.parent.createChildDirectory(null, "subdir") - originalFile.virtualFile.move(null, moveToDirVirtualFile) - } - - assertTrue(isIacUpdateNeeded()) - assertTrue( - "Moved IaC file with issues should been marked as obsolete here", - iacCacheInvalidatedForFilePath(originalFilePath) - ) - } - - @Test - fun `test current IacResults should drop cache and mark IacScanNeeded when IaC supported file RENAMED`() { - val originalFileName = "existing.yaml" - val originalFile = myFixture.addFileToProject(originalFileName, "some text") - val originalFilePath = originalFile.virtualFile.path - createFakeIacResultInCache(originalFileName, originalFilePath) - - assertFalse(isIacUpdateNeeded()) - - ApplicationManager.getApplication().runWriteAction { - originalFile.virtualFile.rename(null, "renamed_filename.txt") - } - - assertTrue(isIacUpdateNeeded()) - assertTrue( - "Renamed IaC file with issues should been marked as obsolete here", - iacCacheInvalidatedForFilePath(originalFilePath) - ) - } - - @Test - fun `test current IacResults should drop cache and mark IacScanNeeded when IaC supported file DELETED`() { - val originalFileName = "existing.yaml" - val originalFile = myFixture.addFileToProject(originalFileName, "some text") - createFakeIacResultInCache(originalFileName, originalFile.virtualFile.path) - - assertFalse(isIacUpdateNeeded()) - - ApplicationManager.getApplication().runWriteAction { - originalFile.virtualFile.delete(null) - } - - assertTrue(isIacUpdateNeeded()) - assertTrue( - "Deleted IaC file with issues should been marked as obsolete here", - iacCacheInvalidatedForFilePath(originalFile.virtualFile.path) - ) - } - - @Test - fun `test currentIacResults should keep it when out-of-project IaC supported file content changed`() { - val fakeIacResult = IacResult(null) - - val file = myFixture.addFileToProject("exclude/k8s-deployment.yaml", "") - val module = ProjectRootManager.getInstance(project).fileIndex.getModuleForFile(file.virtualFile)!! - PsiTestUtil.addExcludedRoot(module, file.virtualFile.parent) - - getSnykCachedResults(project)?.currentIacResult = fakeIacResult - - // change and save excluded file to trigger BulkFileListener to proceed events - ApplicationManager.getApplication().runWriteAction { - PsiDocumentManager.getInstance(project).getDocument(file)?.setText("updated content") - } - FileDocumentManager.getInstance().saveAllDocuments() - - // dispose virtual pointer manually created before - PsiTestUtil.removeExcludedRoot(module, file.virtualFile.parent) - - assertEquals( - "cached IacResult should NOT be dropped after NON project build file changed", - fakeIacResult, - getSnykCachedResults(project)?.currentIacResult - ) - } -} diff --git a/src/test/kotlin/snyk/iac/IacSuggestionDescriptionPanelTest.kt b/src/test/kotlin/snyk/iac/IacSuggestionDescriptionPanelTest.kt deleted file mode 100644 index ee4c6722e..000000000 --- a/src/test/kotlin/snyk/iac/IacSuggestionDescriptionPanelTest.kt +++ /dev/null @@ -1,69 +0,0 @@ -package snyk.iac - -import com.intellij.openapi.project.Project -import io.mockk.every -import io.mockk.mockk -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import org.junit.Before -import org.junit.Test -import snyk.UIComponentFinder.getJButtonByText - -class IacSuggestionDescriptionPanelTest { - - private lateinit var project: Project - private lateinit var cut: IacSuggestionDescriptionPanel - private lateinit var issue: IacIssue - - @Before - fun setUp() { - issue = IacIssue( - "IacTestIssue", - "TestTitle", - "TestSeverity", - "IacTestIssuePublicString", - "https://TestDocumentation", - 123, - "TestIssue", - "TestImpact", - "TestResolve", - listOf("https://TestReference1", "https://TestReference2"), - listOf("Test Path 1", "Test Path 2") - ) - project = mockk() - every { project.basePath } returns "" - } - - @Test - fun `IacSuggestionDescriptionPanel should have ignore button`() { - val expectedButtonText = "Ignore This Issue" - - cut = IacSuggestionDescriptionPanel(issue, null, project) - - val actualButton = getJButtonByText(cut, expectedButtonText) - assertNotNull("Didn't find button with text $expectedButtonText", actualButton) - } - - @Test - fun `IacSuggestionDescriptionPanel ignore button should call IacIgnoreService on click`() { - val expectedButtonText = "Ignore This Issue" - - cut = IacSuggestionDescriptionPanel(issue, null, project) - - val actualButton = getJButtonByText(cut, expectedButtonText) - assertNotNull("Didn't find Ignore Button", actualButton) - val listener = actualButton!!.actionListeners.first() as IgnoreButtonActionListener - assertEquals(IgnoreButtonActionListener::class, listener::class) - assertEquals(issue.id, listener.issue.id) - } - - @Test - fun `IacSuggestionDescriptionPanel should surface references`() { - cut = IacSuggestionDescriptionPanel(issue, null, project) - - issue.references.stream().forEach { - val label = getJButtonByText(cut, it) - assertNotNull("Didn't find reference $it", label) - } - } -} diff --git a/src/test/kotlin/snyk/iac/IgnoreButtonActionListenerTest.kt b/src/test/kotlin/snyk/iac/IgnoreButtonActionListenerTest.kt deleted file mode 100644 index 8e5789578..000000000 --- a/src/test/kotlin/snyk/iac/IgnoreButtonActionListenerTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package snyk.iac - -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.project.Project -import io.mockk.every -import io.mockk.justRun -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.slot -import io.mockk.unmockkAll -import io.mockk.verify -import junit.framework.TestCase.assertEquals -import org.junit.After -import org.junit.Before -import org.junit.Test -import snyk.common.IgnoreService -import java.awt.event.ActionEvent -import javax.swing.JButton - -class IgnoreButtonActionListenerTest { - private val projectBasePath = "" - private lateinit var issue: IacIssue - private lateinit var project: Project - private lateinit var service: IgnoreService - private lateinit var button: JButton - private lateinit var event: ActionEvent - - @Before - fun setUp() { - unmockkAll() - event = mockk() - service = mockk() - project = mockk() - every { project.basePath } returns projectBasePath - button = JButton("test") - issue = IacIssue( - "issueId", - "issueTitle", - "issueSeverity", - "issuePublicId", - "issueDocumentation", - 1, - "issueIssue", - "issueImpact", - "issueResolve" - ) - button.isEnabled = true - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `actionPerformed should submit an IgnoreAction to be run async`() { - mockkStatic(ProgressManager::class) - val progressManager = mockk() - val cut = IgnoreButtonActionListener(service, issue, null, project) - val taskSlot = slot() - - every { ProgressManager.getInstance() } returns progressManager - justRun { progressManager.run(capture(taskSlot)) } - - cut.actionPerformed(event) - - verify(exactly = 1) { - progressManager.run(any()) - } - - assertEquals(taskSlot.captured.project, project) - assertEquals(taskSlot.captured.e, event) - assertEquals(taskSlot.captured.ignoreService, service) - assertEquals(taskSlot.captured.issue, issue) - } -} diff --git a/src/test/kotlin/snyk/iac/annotator/IacBaseAnnotatorCase.kt b/src/test/kotlin/snyk/iac/annotator/IacBaseAnnotatorCase.kt deleted file mode 100644 index ac0593190..000000000 --- a/src/test/kotlin/snyk/iac/annotator/IacBaseAnnotatorCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.testFramework.fixtures.BasePlatformTestCase -import com.intellij.testFramework.replaceService -import io.mockk.mockk -import io.mockk.unmockkAll -import io.snyk.plugin.pluginSettings -import io.snyk.plugin.resetSettings -import snyk.common.SnykCachedResults -import java.nio.file.Paths - -abstract class IacBaseAnnotatorCase : BasePlatformTestCase() { - - val snykCachedResults: SnykCachedResults = mockk(relaxed = true) - - override fun getTestDataPath(): String { - val resource = IacBaseAnnotatorCase::class.java.getResource("/test-fixtures/iac/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() - project.replaceService(SnykCachedResults::class.java, snykCachedResults, project) - resetSettings(project) - pluginSettings().fileListenerEnabled = false - } - - override fun tearDown() { - unmockkAll() - project.replaceService(SnykCachedResults::class.java, SnykCachedResults(project), project) - super.tearDown() - resetSettings(null) - } -} diff --git a/src/test/kotlin/snyk/iac/annotator/IacHclAnnotatorTest.kt b/src/test/kotlin/snyk/iac/annotator/IacHclAnnotatorTest.kt deleted file mode 100644 index 463842446..000000000 --- a/src/test/kotlin/snyk/iac/annotator/IacHclAnnotatorTest.kt +++ /dev/null @@ -1,124 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import junit.framework.TestCase -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile -import snyk.iac.IacResult - -class IacHclAnnotatorTest : IacBaseAnnotatorCase() { - - private val annotationHolderMock = mockk(relaxed = true) - private val terraformManifestFile = "terraform-main.tf" - lateinit var file: VirtualFile - private lateinit var psiFile: PsiFile - - override fun setUp() { - super.setUp() - file = myFixture.copyFileToProject(terraformManifestFile) - psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - } - - fun `test doAnnotation should not return any annotations if no iac issue exists`() { - every { snykCachedResults.currentIacResult } returns null - - val annotations = IacHclAnnotator().getIssues(psiFile) - - assertEquals(0, annotations.size) - } - - fun `test getIssues should return one annotation if only one iac issue exists`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine1() - - val annotations = IacHclAnnotator().getIssues(psiFile) - - TestCase.assertEquals(1, annotations.size) - } - - fun `test apply should trigger newAnnotation call`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine1() - - IacHclAnnotator().apply(psiFile, Unit, annotationHolderMock) - - verify { annotationHolderMock.newAnnotation(any(), any()) } - } - - fun `test textRange without leading whitespace for HCLIdentifier`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine1() - - val expectedRange = TextRange.create(0, 8) - val actualRange = IacHclAnnotator().textRange( - psiFile, - snykCachedResults.currentIacResult?.allCliIssues!!.first().infrastructureAsCodeIssues.first() - ) - - assertEquals(expectedRange, actualRange) - } - - fun `test textRange with leading whitespace for HCLIdentifier`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine8() - - val expectedRange = TextRange.create(201, 209) - val actualRange = IacHclAnnotator().textRange( - psiFile, - snykCachedResults.currentIacResult?.allCliIssues!!.first().infrastructureAsCodeIssues.first() - ) - - assertEquals(expectedRange, actualRange) - } - - fun `test textRange for leading whitespace for HCLProperty`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine18() - - val expectedRange = TextRange.create(446, 453) - val actualRange = IacHclAnnotator().textRange( - psiFile, - snykCachedResults.currentIacResult?.allCliIssues!!.first().infrastructureAsCodeIssues.first() - ) - - assertEquals(expectedRange, actualRange) - } - - private fun createIacResultWithIssueOnLine1(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-TF-74", - title = "Credentials are configured via provider attributes", - lineNumber = 1, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), terraformManifestFile, file.path, "terraform", null, project) - return IacResult(listOf(iacIssuesForFile)) - } - - private fun createIacResultWithIssueOnLine8(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-TF-8", - title = "Snyk - IAM password should contain lowercase", - lineNumber = 8, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), terraformManifestFile, file.path, "terraform", null, project) - return IacResult(listOf(iacIssuesForFile)) - } - - private fun createIacResultWithIssueOnLine18(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-TF-1", - title = "Snyk - Security Group allows open ingress", - lineNumber = 18, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), terraformManifestFile, file.path, "terraform", null, project) - return IacResult(listOf(iacIssuesForFile)) - } -} diff --git a/src/test/kotlin/snyk/iac/annotator/IacJsonAnnotatorTest.kt b/src/test/kotlin/snyk/iac/annotator/IacJsonAnnotatorTest.kt deleted file mode 100644 index b56ab51e6..000000000 --- a/src/test/kotlin/snyk/iac/annotator/IacJsonAnnotatorTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.lang.annotation.HighlightSeverity -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import io.snyk.plugin.pluginSettings -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile -import snyk.iac.IacResult - -@Suppress("FunctionName") -class IacJsonAnnotatorTest : IacBaseAnnotatorCase() { - - private val annotationHolderMock = mockk(relaxed = true) - private val cloudformationManifestFile = "cloudformation-deployment.yaml" - lateinit var file: VirtualFile - private lateinit var psiFile: PsiFile - - override fun setUp() { - super.setUp() - file = myFixture.copyFileToProject(cloudformationManifestFile) - psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - } - - fun `test getIssues should not return any annotations if no iac issue exists`() { - every { snykCachedResults.currentIacResult } returns null - - val issues = IacJsonAnnotator().getIssues(psiFile) - - assertEquals(0, issues.size) - } - - fun `test getIssues should return one annotations if only one iac issue exists`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine2() - - val issues = IacJsonAnnotator().getIssues(psiFile) - - assertEquals(1, issues.size) - } - - fun `test apply should trigger newAnnotation call`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine2() - - IacJsonAnnotator().apply(psiFile, Unit, annotationHolderMock) - - verify { annotationHolderMock.newAnnotation(any(), any()) } - } - - fun `test apply for disabled Severity should not trigger newAnnotation call`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine2() - pluginSettings().mediumSeverityEnabled = false - - IacJsonAnnotator().apply(psiFile, Unit, annotationHolderMock) - - verify(exactly = 0) { annotationHolderMock.newAnnotation(HighlightSeverity.WARNING, any()) } - } - - private fun createIacResultWithIssueOnLine2(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-AWS-426", - title = "Snyk - EC2 API termination protection is not enabled", - lineNumber = 2, - severity = "medium", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), cloudformationManifestFile, file.path, "cloudformation", null, project) - return IacResult(listOf(iacIssuesForFile)) - } -} diff --git a/src/test/kotlin/snyk/iac/annotator/IacYamlAnnotatorTest.kt b/src/test/kotlin/snyk/iac/annotator/IacYamlAnnotatorTest.kt deleted file mode 100644 index 845115dca..000000000 --- a/src/test/kotlin/snyk/iac/annotator/IacYamlAnnotatorTest.kt +++ /dev/null @@ -1,99 +0,0 @@ -package snyk.iac.annotator - -import com.intellij.lang.annotation.AnnotationHolder -import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.util.TextRange -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiFile -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile -import snyk.iac.IacResult - -class IacYamlAnnotatorTest : IacBaseAnnotatorCase() { - - private val annotationHolderMock = mockk(relaxed = true) - private val kubernetesManifestFile = "kubernetes-deployment.yaml" - lateinit var file: VirtualFile - private lateinit var psiFile: PsiFile - - override fun setUp() { - super.setUp() - file = myFixture.copyFileToProject(kubernetesManifestFile) - psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - } - - fun `test getIssues should not return any annotations if no iac issue exists`() { - every { snykCachedResults.currentIacResult } returns null - - val issues = IacYamlAnnotator().getIssues(psiFile) - - assertEquals(0, issues.size) - } - - fun `test getIssues should return one annotations if only one iac issue exists`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine18() - - val issues = IacYamlAnnotator().getIssues(psiFile) - - assertEquals(1, issues.size) - } - - fun `test apply should trigger newAnnotation call`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine18() - - IacYamlAnnotator().apply(psiFile, Unit, annotationHolderMock) - - verify { annotationHolderMock.newAnnotation(any(), any()) } - } - - fun `test textRange with leading space`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine18() - - val expectedRange = TextRange.create(262, 269) - val actualRange = IacYamlAnnotator().textRange( - psiFile, - snykCachedResults.currentIacResult?.allCliIssues!!.first().infrastructureAsCodeIssues.first() - ) - - assertEquals(expectedRange, actualRange) - } - - fun `test textRange with dash at the begin`() { - every { snykCachedResults.currentIacResult } returns createIacResultWithIssueOnLine20() - - val expectedRange = TextRange.create(304, 308) - val actualRange = IacYamlAnnotator().textRange( - psiFile, - snykCachedResults.currentIacResult?.allCliIssues!!.first().infrastructureAsCodeIssues.first() - ) - - assertEquals(expectedRange, actualRange) - } - - private fun createIacResultWithIssueOnLine18(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-K8S-13", - title = "Container is running in host's IPC namespace", - lineNumber = 18, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), kubernetesManifestFile, file.path, "Kubernetes", null, project) - return IacResult(listOf(iacIssuesForFile)) - } - - private fun createIacResultWithIssueOnLine20(): IacResult { - val iacIssue = IacIssue( - id = "SNYK-CC-K8S-4", - title = "Container is running without cpu limit", - lineNumber = 20, - severity = "", publicId = "", documentation = "", issue = "", impact = "" - ) - val iacIssuesForFile = - IacIssuesForFile(listOf(iacIssue), kubernetesManifestFile, file.path, "Kubernetes", null, project) - return IacResult(listOf(iacIssuesForFile)) - } -} From f89e870fd44456e671dca9ffda56e73aa0f0d8d3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 11:16:54 +0200 Subject: [PATCH 42/51] chore: remove unused imports --- .../snyk/plugin/services/SnykTaskQueueServiceTest.kt | 2 -- .../ui/toolwindow/SnykToolWindowPanelIntegTest.kt | 10 ---------- .../SuggestionDescriptionPanelFromLSCodeTest.kt | 2 -- .../SuggestionDescriptionPanelFromLSOSSTest.kt | 3 --- .../snyk/common/lsp/LanguageServerWrapperTest.kt | 1 - 5 files changed, 18 deletions(-) diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt index a09ca0166..af5d9d456 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykTaskQueueServiceTest.kt @@ -12,7 +12,6 @@ import io.mockk.unmockkAll import io.mockk.verify import io.snyk.plugin.getCliFile import io.snyk.plugin.getContainerService -import io.snyk.plugin.getIacService import io.snyk.plugin.getSnykCachedResults import io.snyk.plugin.getSnykCliDownloaderService import io.snyk.plugin.isCliInstalled @@ -26,7 +25,6 @@ import org.awaitility.Awaitility.await import org.eclipse.lsp4j.services.LanguageServer import snyk.common.lsp.LanguageServerWrapper import snyk.container.ContainerResult -import snyk.iac.IacResult import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.TimeUnit 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 3b9b8f53f..d420a0b8a 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelIntegTest.kt @@ -4,11 +4,9 @@ package io.snyk.plugin.ui.toolwindow import com.intellij.ide.util.gotoByName.GotoFileCellRenderer import com.intellij.mock.MockVirtualFile -import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.components.service import com.intellij.testFramework.HeavyPlatformTestCase import com.intellij.testFramework.PlatformTestUtil -import com.intellij.testFramework.TestActionEvent import com.intellij.util.ui.tree.TreeUtil import io.mockk.every import io.mockk.mockk @@ -16,7 +14,6 @@ import io.mockk.mockkObject import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify -import io.snyk.plugin.Severity import io.snyk.plugin.events.SnykResultsFilteringListener import io.snyk.plugin.events.SnykScanListener import io.snyk.plugin.events.SnykScanListenerLS @@ -32,7 +29,6 @@ import io.snyk.plugin.resetSettings import io.snyk.plugin.services.SnykTaskQueueService import io.snyk.plugin.setupDummyCliFile import io.snyk.plugin.ui.SnykBalloonNotificationHelper -import io.snyk.plugin.ui.actions.SnykTreeMediumSeverityFilterAction import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel import org.eclipse.lsp4j.ExecuteCommandParams @@ -53,15 +49,9 @@ import snyk.container.ui.BaseImageRemediationDetailPanel import snyk.container.ui.ContainerImageTreeNode import snyk.container.ui.ContainerIssueDetailPanel import snyk.container.ui.ContainerIssueTreeNode -import snyk.iac.IacError -import snyk.iac.IacIssue -import snyk.iac.IacIssuesForFile import snyk.iac.IacResult -import snyk.iac.IacScanService -import snyk.iac.IgnoreButtonActionListener import snyk.trust.confirmScanningAndSetWorkspaceTrustedStateIfNeeded import java.util.concurrent.CompletableFuture -import javax.swing.JButton import javax.swing.JEditorPane import javax.swing.JLabel import javax.swing.JPanel 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 c16e5a30c..512b0a9dd 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSCodeTest.kt @@ -19,8 +19,6 @@ import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range import snyk.UIComponentFinder.getJBCEFBrowser import snyk.UIComponentFinder.getJLabelByText -import snyk.UIComponentFinder.getJPanelByName -import snyk.common.ProductType import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.CommitChangeLine import snyk.common.lsp.DataFlow 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 3cc4f6d25..619dacd68 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -15,10 +15,7 @@ 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 snyk.UIComponentFinder.getActionLinkByText import snyk.UIComponentFinder.getJLabelByText -import snyk.UIComponentFinder.getJPanelByName -import snyk.common.ProductType import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 3c78c5a4b..158d0a2dd 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -2,7 +2,6 @@ package snyk.common.lsp import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.impl.AnyModalityState import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager From 8598677936665d4366018efc6f8658c4224a8135 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 11:57:11 +0200 Subject: [PATCH 43/51] fix: compile errors --- .../plugin/analytics/AnalyticsScanListener.kt | 16 ---------------- .../snyk/plugin/ui/toolwindow/SnykToolWindow.kt | 4 ---- 2 files changed, 20 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt index 383c0b86f..8f500adb4 100644 --- a/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt +++ b/src/main/kotlin/io/snyk/plugin/analytics/AnalyticsScanListener.kt @@ -44,18 +44,6 @@ class AnalyticsScanListener(val project: Project) { start = System.currentTimeMillis() } - override fun scanningIacFinished(iacResult: IacResult) { - val scanDoneEvent = getScanDoneEvent( - System.currentTimeMillis() - start, - "Snyk IaC", - iacResult.criticalSeveritiesCount(), - iacResult.highSeveritiesCount(), - iacResult.mediumSeveritiesCount(), - iacResult.lowSeveritiesCount() - ) - LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent) - } - override fun scanningContainerFinished(containerResult: ContainerResult) { val scanDoneEvent = getScanDoneEvent( System.currentTimeMillis() - start, @@ -68,10 +56,6 @@ class AnalyticsScanListener(val project: Project) { LanguageServerWrapper.getInstance().sendReportAnalyticsCommand(scanDoneEvent) } - override fun scanningIacError(snykError: SnykError) { - // do nothing - } - override fun scanningContainerError(snykError: SnykError) { // do nothing } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt index 18409b502..7dd24a993 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindow.kt @@ -72,10 +72,6 @@ class SnykToolWindow(private val project: Project) : SimpleToolWindowPanel(false override fun scanningStarted() = updateActionsPresentation() - override fun scanningIacFinished(iacResult: IacResult) = updateActionsPresentation() - - override fun scanningIacError(snykError: SnykError) = updateActionsPresentation() - override fun scanningContainerFinished(containerResult: ContainerResult) = updateActionsPresentation() override fun scanningContainerError(snykError: SnykError) = updateActionsPresentation() From 85a49d31f6f054e9336ee4346e60f5560c64b6bf Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 12:37:25 +0200 Subject: [PATCH 44/51] fix: filter out excluded files from results --- src/main/kotlin/io/snyk/plugin/SnykFile.kt | 4 ++++ src/main/kotlin/io/snyk/plugin/Utils.kt | 8 ++++++++ src/main/kotlin/snyk/common/SnykCachedResults.kt | 2 +- src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt | 5 +++-- .../kotlin/snyk/common/codevision/LSCodeVisionProvider.kt | 3 +++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/SnykFile.kt b/src/main/kotlin/io/snyk/plugin/SnykFile.kt index 7a2e28e46..190d8e426 100644 --- a/src/main/kotlin/io/snyk/plugin/SnykFile.kt +++ b/src/main/kotlin/io/snyk/plugin/SnykFile.kt @@ -7,6 +7,10 @@ import snyk.common.RelativePathHelper data class SnykFile(val project: Project, val virtualFile: VirtualFile) { val relativePath = runAsync { RelativePathHelper().getRelativePath(virtualFile, project) } + + fun isInContent(): Boolean { + return this.virtualFile.isInContent(project) + } } fun toSnykFileSet(project: Project, virtualFiles: Set) = diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index b82f2ef41..768f90da6 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -5,6 +5,7 @@ package io.snyk.plugin import com.intellij.codeInsight.codeVision.CodeVisionHost import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer +import com.intellij.icons.ExpUiIcons.Run import com.intellij.ide.util.PsiNavigationSupport import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PathManager @@ -20,6 +21,7 @@ import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.progress.ProgressIndicator import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.registry.Registry @@ -32,6 +34,7 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.util.Alarm import com.intellij.util.messages.Topic +import com.jetbrains.rd.generator.nova.PredefinedType import io.snyk.plugin.analytics.AnalyticsScanListener import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.services.SnykCliAuthenticationService @@ -449,3 +452,8 @@ fun Project.getContentRootVirtualFiles(): Set { .filter { it.exists() && it.isDirectory } .sortedBy { it.path }.toSet() } + +fun VirtualFile.isInContent(project: Project): Boolean { + val vf = this + return ReadAction.compute { ProjectFileIndex.getInstance(project).isInContent(vf) } +} diff --git a/src/main/kotlin/snyk/common/SnykCachedResults.kt b/src/main/kotlin/snyk/common/SnykCachedResults.kt index 642626549..b68212ef9 100644 --- a/src/main/kotlin/snyk/common/SnykCachedResults.kt +++ b/src/main/kotlin/snyk/common/SnykCachedResults.kt @@ -168,7 +168,7 @@ class SnykCachedResults( snykFile: SnykFile, issueList: List ) { - + if (!snykFile.isInContent()) return when (product) { LsProductConstants.OpenSource.value -> { currentOSSResultsLS[snykFile] = issueList diff --git a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt index fe552c275..5b37a4ead 100644 --- a/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt +++ b/src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt @@ -23,6 +23,7 @@ import icons.SnykIcons import io.snyk.plugin.Severity import io.snyk.plugin.getSnykCachedResultsForProduct import io.snyk.plugin.getSnykToolWindowPanel +import io.snyk.plugin.isInContent import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeAction import org.eclipse.lsp4j.CodeActionContext @@ -43,7 +44,6 @@ import snyk.common.annotator.SnykAnnotator.SnykAnnotation import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.RangeConverter import snyk.common.lsp.ScanIssue -import java.util.Collections import java.util.concurrent.TimeUnit import java.util.concurrent.TimeoutException import javax.swing.Icon @@ -249,7 +249,8 @@ abstract class SnykAnnotator(private val product: ProductType) : private fun getIssuesForFile(psiFile: PsiFile): Set = getSnykCachedResultsForProduct(psiFile.project, product) ?.filter { - it.key.virtualFile == psiFile.virtualFile + val virtualFile = it.key.virtualFile + virtualFile == psiFile.virtualFile && virtualFile.isInContent(psiFile.project) } ?.map { it.value } ?.flatten() diff --git a/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt b/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt index 1237768a1..0c81d81cf 100644 --- a/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt +++ b/src/main/kotlin/snyk/common/codevision/LSCodeVisionProvider.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import icons.SnykIcons +import io.snyk.plugin.isInContent import io.snyk.plugin.toLanguageServerURL import org.eclipse.lsp4j.CodeLens import org.eclipse.lsp4j.CodeLensParams @@ -47,6 +48,7 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro if (LanguageServerWrapper.getInstance().isDisposed()) return CodeVisionState.READY_EMPTY if (!LanguageServerWrapper.getInstance().isInitialized) return CodeVisionState.READY_EMPTY val project = editor.project ?: return CodeVisionState.READY_EMPTY + if (!editor.virtualFile.isInContent(project)) return CodeVisionState.READY_EMPTY val document = editor.document @@ -54,6 +56,7 @@ class LSCodeVisionProvider : CodeVisionProvider, CodeVisionGroupSettingPro PsiDocumentManager.getInstance(project).getPsiFile(document) } ?: return CodeVisionState.READY_EMPTY + val params = CodeLensParams(TextDocumentIdentifier(file.virtualFile.toLanguageServerURL())) val lenses = mutableListOf>() val codeLenses = try { From 75f84651dec9b6d077be01f3a7326008592eacfa Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 12:37:52 +0200 Subject: [PATCH 45/51] chore: remove unused imports --- src/main/kotlin/io/snyk/plugin/Utils.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/Utils.kt b/src/main/kotlin/io/snyk/plugin/Utils.kt index 768f90da6..b4cc01b0c 100644 --- a/src/main/kotlin/io/snyk/plugin/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/Utils.kt @@ -5,7 +5,6 @@ package io.snyk.plugin import com.intellij.codeInsight.codeVision.CodeVisionHost import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer -import com.intellij.icons.ExpUiIcons.Run import com.intellij.ide.util.PsiNavigationSupport import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PathManager @@ -34,7 +33,6 @@ import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager import com.intellij.util.Alarm import com.intellij.util.messages.Topic -import com.jetbrains.rd.generator.nova.PredefinedType import io.snyk.plugin.analytics.AnalyticsScanListener import io.snyk.plugin.services.SnykApplicationSettingsStateService import io.snyk.plugin.services.SnykCliAuthenticationService From 3aedb54c11e767e975fbb7eb798353ae857e47c5 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 12:43:32 +0200 Subject: [PATCH 46/51] fix: visiblity of methods --- .../snyk/common/editor/LineEndingEditorFactoryListener.kt | 2 +- src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt index 0d72f2101..a4756a0bb 100644 --- a/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt +++ b/src/main/kotlin/snyk/common/editor/LineEndingEditorFactoryListener.kt @@ -98,7 +98,7 @@ class LineEndingEditorFactoryListener : EditorFactoryListener, Disposable { val project = editor.project val document = editor.document - val snykCachedResults = project?.let { getSnykCachedResults(it) } + private val snykCachedResults = project?.let { getSnykCachedResults(it) } val languageServerWrapper = LanguageServerWrapper.getInstance() val logger = Logger.getInstance(this::class.java).apply { // tie log level to language server log level diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 292a5f182..1e0ba275a 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -114,7 +114,7 @@ class SnykLanguageClient : return } - private fun getScanIssues(diagnosticsParams: PublishDiagnosticsParams): List { + fun getScanIssues(diagnosticsParams: PublishDiagnosticsParams): List { val issueList = diagnosticsParams.diagnostics.stream().map { val issue = Gson().fromJson(it.data.toString(), ScanIssue::class.java) // load textrange for issue so it doesn't happen in UI thread From 71720a79711153537f299ff102af69fa61cfc129 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 12:43:47 +0200 Subject: [PATCH 47/51] fix: code smell --- src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index 2d38629a0..5f954992e 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -151,8 +151,8 @@ class SnykLanguageClientTest { // Assert the returned list contains parsed ScanIssues assertEquals(2, result.size) - assertEquals("Some Issue", result.get(0).title) - assertEquals("Another Issue", result.get(1).title) + assertEquals("Some Issue", result[0].title) + assertEquals("Another Issue", result[1].title) } private fun createMockDiagnostic(range: Range, message: String): Diagnostic { From 18f04cda9641c018c02e9befa59415753288b206 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 14:07:54 +0200 Subject: [PATCH 48/51] fix: tests --- .../plugin/services/SnykTaskQueueService.kt | 12 ++---- src/main/kotlin/snyk/common/lsp/Types.kt | 1 - .../ui/toolwindow/SnykToolWindowPanelTest.kt | 2 - .../snyk/common/lsp/SnykLanguageClientTest.kt | 37 ++++++++++++++----- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt index 534d2b22d..af89ab151 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykTaskQueueService.kt @@ -54,6 +54,8 @@ class SnykTaskQueueService(val project: Project) { fun connectProjectToLanguageServer(project: Project) { // subscribe to the settings changed topic val languageServerWrapper = LanguageServerWrapper.getInstance() + languageServerWrapper.ensureLanguageServerInitialized() + getSnykToolWindowPanel(project)?.let { project.messageBus.connect(it) .subscribe( @@ -65,16 +67,8 @@ class SnykTaskQueueService(val project: Project) { } ) } - // Try to connect project for up to 30s - for (tries in 1..300) { - if (!languageServerWrapper.isInitialized) { - Thread.sleep(100) - continue - } - languageServerWrapper.addContentRoots(project) - break - } + languageServerWrapper.addContentRoots(project) } fun scan() { diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 550edbe9c..cffbf5f38 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -450,7 +450,6 @@ data class IssueData( @SerializedName("ruleId") val ruleId: String, @SerializedName("details") val details: String?, ) { - override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 d5530be34..90149c534 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanelTest.kt @@ -109,8 +109,6 @@ class SnykToolWindowPanelTest : LightPlatform4TestCase() { val descriptionPanel = UIComponentFinder.getJPanelByName(cut, "descriptionPanel") assertNotNull(descriptionPanel) assertEquals(findOnePixelSplitter(vulnerabilityTree), descriptionPanel!!.parent) - - verify(exactly = 1) { taskQueueService.scan() } } //TODO rewrite diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index 5f954992e..eb3a6850a 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -1,21 +1,27 @@ package snyk.common.lsp +import com.google.gson.Gson import com.intellij.openapi.application.Application import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Document import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.vfs.VirtualFile import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll import io.mockk.verify +import io.snyk.plugin.getDocument import io.snyk.plugin.pluginSettings import io.snyk.plugin.services.SnykApplicationSettingsStateService +import io.snyk.plugin.toVirtualFile import io.snyk.plugin.ui.toolwindow.SnykPluginDisposable import org.eclipse.lsp4j.Diagnostic import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.Range import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -23,13 +29,13 @@ import org.junit.Before import org.junit.Test import snyk.pluginInfo import snyk.trust.WorkspaceTrustService +import java.nio.file.Files import kotlin.io.path.Path -import org.eclipse.lsp4j.Range class SnykLanguageClientTest { private lateinit var cut: SnykLanguageClient - private var snykPluginDisposable = SnykPluginDisposable() + private var snykPluginDisposable: SnykPluginDisposable? = null private val applicationMock: Application = mockk() private val projectMock: Project = mockk() @@ -48,16 +54,19 @@ class SnykLanguageClientTest { every { applicationMock.getService(WorkspaceTrustService::class.java) } returns trustServiceMock every { applicationMock.getService(ProjectManager::class.java) } returns projectManagerMock - every { applicationMock.getService(SnykPluginDisposable::class.java) } returns snykPluginDisposable every { applicationMock.isDisposed } returns false every { applicationMock.messageBus } returns mockk(relaxed = true) + snykPluginDisposable = SnykPluginDisposable() + every { applicationMock.getService(SnykPluginDisposable::class.java) } returns snykPluginDisposable!! + every { projectManagerMock.openProjects } returns arrayOf(projectMock) every { projectMock.isDisposed } returns false every { projectMock.getService(DumbService::class.java) } returns dumbServiceMock every { dumbServiceMock.isDumb } returns false every { pluginSettings() } returns settings + mockkStatic("snyk.PluginInformationKt") every { pluginInfo } returns mockk(relaxed = true) every { pluginInfo.integrationName } returns "Snyk Intellij Plugin" @@ -65,7 +74,6 @@ class SnykLanguageClientTest { every { pluginInfo.integrationEnvironment } returns "IntelliJ IDEA" every { pluginInfo.integrationEnvironmentVersion } returns "2020.3.2" - snykPluginDisposable = SnykPluginDisposable() cut = SnykLanguageClient() } @@ -139,12 +147,21 @@ class SnykLanguageClientTest { @Test fun testGetScanIssues_validDiagnostics() { + val tempFile = Files.createTempFile("testGetScanIssues", ".java") + val filePath = tempFile.fileName.toString() + val vf = mockk(relaxed = true) + every { filePath.toVirtualFile() } returns vf + val document = mockk(relaxed = true) + every { vf.getDocument() } returns document + every { document.getLineStartOffset(any())} returns 0 + + val uri = tempFile.toUri().toString() val range = Range(Position(0, 0), Position(0, 1)) // Create mock PublishDiagnosticsParams with diagnostics list val diagnostics: MutableList = ArrayList() - diagnostics.add(createMockDiagnostic(range, "Some Issue")) - diagnostics.add(createMockDiagnostic(range, "Another Issue")) - val diagnosticsParams = PublishDiagnosticsParams("test", diagnostics) + diagnostics.add(createMockDiagnostic(range, "Some Issue", filePath)) + diagnostics.add(createMockDiagnostic(range, "Another Issue", filePath)) + val diagnosticsParams = PublishDiagnosticsParams(uri, diagnostics) // Call scanIssues function val result: List = cut.getScanIssues(diagnosticsParams) @@ -155,9 +172,9 @@ class SnykLanguageClientTest { assertEquals("Another Issue", result[1].title) } - private fun createMockDiagnostic(range: Range, message: String): Diagnostic { - val jsonString = """{"id": 12345, "title": "$message"}""" - + private fun createMockDiagnostic(range: Range, message: String, filePath: String): Diagnostic { + val rangeString = Gson().toJson(range) + val jsonString = """{"id": 12345, "title": "$message", "filePath": "$filePath", "range": $rangeString}""" val mockDiagnostic = Diagnostic() mockDiagnostic.range = range mockDiagnostic.data = jsonString From 8468d06717124d6f61aba78c9532fdee9c5f5cd3 Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 14:41:24 +0200 Subject: [PATCH 49/51] feat: allow usage of plugin 2024.3 (EAP+) --- CHANGELOG.md | 1 + gradle.properties | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 909145a32..80bab086d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - display documentation info when hovering over issue - add inline value support for display of vulnerability count - added ai fix feedback support +- enable for IntelliJ 2024.3 platform ### Fixes - add name to code vision provider diff --git a/gradle.properties b/gradle.properties index 32f3113db..637516228 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginName=Snyk Security # for insight into build numbers and IntelliJ Platform versions # see https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild=233 -pluginUntilBuild=242.* +pluginUntilBuild=243.* platformVersion=2023.3 platformDownloadSources=true From e07cb7ba104b2e0c70a4409e3c65b23f41f11c8f Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 15:13:47 +0200 Subject: [PATCH 50/51] fix: don't use kotlin import --- src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index eb3a6850a..8fd68f46c 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -30,7 +30,8 @@ import org.junit.Test import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import java.nio.file.Files -import kotlin.io.path.Path +import java.nio.file.Path +import java.nio.file.Paths class SnykLanguageClientTest { @@ -142,7 +143,7 @@ class SnykLanguageClientTest { val path = "abc" cut.addTrustedPaths(SnykTrustedFoldersParams(listOf(path))) - verify { trustServiceMock.addTrustedPath(eq(Path(path))) } + verify { trustServiceMock.addTrustedPath(Paths.get(path)) } } @Test From 602cf9669a2038628891e511c29107ea8df60bbf Mon Sep 17 00:00:00 2001 From: Bastian Doetsch Date: Wed, 2 Oct 2024 15:14:28 +0200 Subject: [PATCH 51/51] chore: remove unused import --- src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index 8fd68f46c..6d3dccd51 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -30,7 +30,6 @@ import org.junit.Test import snyk.pluginInfo import snyk.trust.WorkspaceTrustService import java.nio.file.Files -import java.nio.file.Path import java.nio.file.Paths