From 2581e2dc2e8722a960d0f0095377b7912a3789fe Mon Sep 17 00:00:00 2001 From: Teodora Sandu <81559517+teodora-sandu@users.noreply.github.com> Date: Mon, 17 Jun 2024 15:43:46 +0100 Subject: [PATCH] fix: dataflow links work across files [IDE-391] (#550) * fix: dataflow links work across files * refactor: pre-compute virtual files outside of UI thread --- CHANGELOG.md | 1 + .../ui/jcef/OpenFileLoadHandlerGenerator.kt | 80 +++++++++++-------- .../kotlin/io/snyk/plugin/ui/jcef/Utils.kt | 14 ++-- .../toolwindow/panels/JCEFDescriptionPanel.kt | 29 +++++-- .../jcef/OpenFileLoadHandlerGeneratorTest.kt | 10 +-- ...SuggestionDescriptionPanelFromLSOSSTest.kt | 3 + 6 files changed, 85 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89535237..c0084b3d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [2.8.6] ### Fixed - automatically migrate old-format endpoint to https://api.xxx.snyk.io endpoint and save it in settings. Also, add tooltip to custom endpoint field explaining the format. +- fix multi-file links in the DataFlow HTML panel ## [2.8.5] ### Fixed diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGenerator.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGenerator.kt index 54286f08f..fa419941d 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGenerator.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGenerator.kt @@ -3,29 +3,33 @@ package io.snyk.plugin.ui.jcef import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.colors.ColorKey import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import com.intellij.ui.JBColor import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.ui.jcef.JBCefJSQuery import com.intellij.util.ui.UIUtil import io.snyk.plugin.getDocument import io.snyk.plugin.navigateToSource -import io.snyk.plugin.SnykFile import org.cef.browser.CefBrowser import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import java.awt.Color -class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { - private val project = snykFile.project - private val virtualFile = snykFile.virtualFile - +class OpenFileLoadHandlerGenerator( + private val project: Project, + private val virtualFiles: LinkedHashMap, +) { fun openFile(value: String): JBCefJSQuery.Response { val values = value.replace("\n", "").split(":") + val filePath = values[0] val startLine = values[1].toInt() val endLine = values[2].toInt() val startCharacter = values[3].toInt() val endCharacter = values[4].toInt() + val virtualFile = virtualFiles[filePath] ?: return JBCefJSQuery.Response("success") + ApplicationManager.getApplication().invokeLater { val document = virtualFile.getDocument() val startLineStartOffset = document?.getLineStartOffset(startLine) ?: 0 @@ -43,35 +47,48 @@ class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { return "#%02x%02x%02x".format(color.red, color.green, color.blue) } - fun shift(colorComponent: Int, d: Double): Int { + fun shift( + colorComponent: Int, + d: Double, + ): Int { val n = (colorComponent * d).toInt() return n.coerceIn(0, 255) } - fun getCodeDiffColors(baseColor: Color, isHighContrast: Boolean): Pair { - val addedColor = if (isHighContrast) { - Color(28, 68, 40) // high contrast green - } else { - Color(shift(baseColor.red, 0.75), baseColor.green, shift(baseColor.blue, 0.75)) - } + fun getCodeDiffColors( + baseColor: Color, + isHighContrast: Boolean, + ): Pair { + val addedColor = + if (isHighContrast) { + Color(28, 68, 40) // high contrast green + } else { + Color(shift(baseColor.red, 0.75), baseColor.green, shift(baseColor.blue, 0.75)) + } - val removedColor = if (isHighContrast) { - Color(84, 36, 38) // high contrast red - } else { - Color(shift(baseColor.red, 1.25), shift(baseColor.green, 0.85), shift(baseColor.blue, 0.85)) - } + val removedColor = + if (isHighContrast) { + Color(84, 36, 38) // high contrast red + } else { + Color(shift(baseColor.red, 1.25), shift(baseColor.green, 0.85), shift(baseColor.blue, 0.85)) + } return Pair(addedColor, removedColor) } fun generate(jbCefBrowser: JBCefBrowserBase): CefLoadHandlerAdapter { val openFileQuery = JBCefJSQuery.create(jbCefBrowser) val isDarkTheme = EditorColorsManager.getInstance().isDarkEditor - val isHighContrast = EditorColorsManager.getInstance().globalScheme.name.contains("High contrast", ignoreCase = true) + val isHighContrast = + EditorColorsManager.getInstance().globalScheme.name.contains("High contrast", ignoreCase = true) openFileQuery.addHandler { openFile(it) } return object : CefLoadHandlerAdapter() { - override fun onLoadEnd(browser: CefBrowser, frame: CefFrame, httpStatusCode: Int) { + override fun onLoadEnd( + browser: CefBrowser, + frame: CefFrame, + httpStatusCode: Int, + ) { if (frame.isMain) { val script = """ (function() { @@ -89,14 +106,12 @@ class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { target.getAttribute("end-character")); } - // Attach a single event listener to the document - document.addEventListener('click', function(e) { - // Find the nearest ancestor - var target = e.target.closest('.data-flow-clickable-row'); - if (target) { - navigateToIssue(e, target); - } - }); + const dataFlows = document.getElementsByClassName('data-flow-clickable-row'); + for (let i = 0; i < dataFlows.length; i++) { + dataFlows[i].addEventListener('click', (e) => { + navigateToIssue(e, dataFlows[i]); + }); + } document.getElementById('position-line').addEventListener('click', (e) => { // Find the first var target = document.getElementsByClassName('data-flow-clickable-row')[0]; @@ -116,8 +131,10 @@ class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { val dataFlowColor = toCssHex(baseColor) val globalScheme = EditorColorsManager.getInstance().globalScheme - val tearLineColor = globalScheme.getColor(ColorKey.find("TEARLINE_COLOR")) // The closest color to target_rgb = (198, 198, 200) - val tabItemHoverColor = globalScheme.getColor(ColorKey.find("INDENT_GUIDE")) // The closest color to target_rgb = RGB (235, 236, 240) + val tearLineColor = + globalScheme.getColor(ColorKey.find("TEARLINE_COLOR")) // The closest color to target_rgb = (198, 198, 200) + val tabItemHoverColor = + globalScheme.getColor(ColorKey.find("INDENT_GUIDE")) // The closest color to target_rgb = RGB (235, 236, 240) val themeScript = """ (function(){ @@ -142,8 +159,8 @@ class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { } // Add theme class to body - const isDarkTheme = ${isDarkTheme}; - const isHighContrast = ${isHighContrast}; + const isDarkTheme = $isDarkTheme; + const isHighContrast = $isHighContrast; document.body.classList.add(isHighContrast ? 'high-contrast' : (isDarkTheme ? 'dark' : 'light')); })(); """ @@ -153,4 +170,3 @@ class OpenFileLoadHandlerGenerator(snykFile: SnykFile) { } } } - 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 b73ed7634..7176821d3 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/Utils.kt @@ -6,19 +6,19 @@ import com.intellij.ui.jcef.JBCefBrowserBuilder import org.cef.handler.CefLoadHandlerAdapter import java.awt.Component - object JCEFUtils { - fun getJBCefBrowserComponentIfSupported (html: String, loadHandlerGenerator: (jbCefBrowser: JBCefBrowser) -> CefLoadHandlerAdapter): Component? { + fun getJBCefBrowserComponentIfSupported( + html: String, + loadHandlerGenerator: (jbCefBrowser: JBCefBrowser) -> CefLoadHandlerAdapter, + ): Component? { 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() + val jbCefBrowser = + JBCefBrowserBuilder().setClient(cefClient).setEnableOpenDevToolsMenuItem(true) + .setMouseWheelEventEnable(true).build() jbCefBrowser.setOpenLinksInExternalBrowser(true) val loadHandler = loadHandlerGenerator(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 5a11c26bb..c1de8d3c3 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.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.uiDesigner.core.GridLayoutManager import com.intellij.util.ui.JBUI import io.snyk.plugin.SnykFile @@ -18,6 +20,7 @@ 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 @@ -37,7 +40,13 @@ class SuggestionDescriptionPanelFromLS( pluginSettings().isGlobalIgnoresFeatureEnabled && issue.canLoadSuggestionPanelFromHTML() ) { - val openFileLoadHandlerGenerator = OpenFileLoadHandlerGenerator(snykFile) + 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) val html = this.getStyledHTML() val jbCefBrowserComponent = JCEFUtils.getJBCefBrowserComponentIfSupported(html) { @@ -127,13 +136,17 @@ class SuggestionDescriptionPanelFromLS( ideStyle = SnykStylesheets.SnykCodeSuggestion } html = html.replace("\${ideStyle}", "") - html = html.replace("\${ideScript}", "") + html = + html.replace( + "\${ideScript}", + "", + ) val nonce = getNonce() html = html.replace("\${nonce}", nonce) diff --git a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt index 06f5b0284..98c3e8578 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/jcef/OpenFileLoadHandlerGeneratorTest.kt @@ -6,17 +6,15 @@ import com.intellij.psi.PsiFile import com.intellij.testFramework.fixtures.BasePlatformTestCase import io.mockk.unmockkAll import io.snyk.plugin.resetSettings -import io.snyk.plugin.SnykFile import org.junit.Test import snyk.code.annotator.SnykCodeAnnotator import java.nio.file.Paths -class OpenFileLoadHandlerGeneratorTest : BasePlatformTestCase(){ +class OpenFileLoadHandlerGeneratorTest : BasePlatformTestCase() { private lateinit var generator: OpenFileLoadHandlerGenerator private val fileName = "app.js" private lateinit var file: VirtualFile private lateinit var psiFile: PsiFile - private lateinit var snykFile: SnykFile override fun getTestDataPath(): String { val resource = SnykCodeAnnotator::class.java.getResource("/test-fixtures/code/annotator") @@ -31,9 +29,11 @@ class OpenFileLoadHandlerGeneratorTest : BasePlatformTestCase(){ file = myFixture.copyFileToProject(fileName) psiFile = WriteAction.computeAndWait { psiManager.findFile(file)!! } - snykFile = SnykFile(psiFile.project, psiFile.virtualFile) - generator = OpenFileLoadHandlerGenerator(snykFile) + val virtualFiles = LinkedHashMap() + virtualFiles[fileName] = psiFile.virtualFile + + generator = OpenFileLoadHandlerGenerator(psiFile.project, virtualFiles) } @Test 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 23ef95eac..3dc28eb62 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SuggestionDescriptionPanelFromLSOSSTest.kt @@ -72,6 +72,9 @@ class SuggestionDescriptionPanelFromLSOSSTest : BasePlatformTestCase() { every { issue.additionalData.fixedIn } returns listOf("fixedIn") every { issue.additionalData.exploit } returns "exploit" every { issue.additionalData.description } returns "description" + every { + issue.additionalData.dataFlow + } returns emptyList() } @Test