Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: (LS Preview) Snyk Code scans when having multiple projects open [IDE-160] #482

Merged
merged 4 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [2.7.7]
### Fixed
- (LS Preview) Snyk Code scans when having multiple projects open
- (LS Preview) do not hang on missing answers to message requests, timeout after 5s
- Provide language-specific annotators for Snyk Code issues

Expand Down
18 changes: 14 additions & 4 deletions src/main/kotlin/io/snyk/plugin/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ import snyk.advisor.AdvisorService
import snyk.advisor.AdvisorServiceImpl
import snyk.advisor.SnykAdvisorModel
import snyk.amplitude.AmplitudeExperimentService
import snyk.common.ProductType
import snyk.common.SnykCachedResults
import snyk.common.UIComponentFinder
import snyk.common.isSnykTenant
import snyk.common.lsp.ScanInProgressKey
import snyk.common.lsp.ScanState
import snyk.container.ContainerService
import snyk.container.KubernetesImageCache
Expand Down Expand Up @@ -177,10 +179,18 @@ fun isOssRunning(project: Project): Boolean {
return indicator != null && indicator.isRunning && !indicator.isCanceled
}

fun isSnykCodeRunning(project: Project): Boolean =
AnalysisData.instance.isUpdateAnalysisInProgress(project)
|| RunUtils.instance.isFullRescanRequested(project)
|| ScanState.scanInProgress[ScanState.SNYK_CODE] == true
fun isSnykCodeRunning(project: Project): Boolean {
val lsRunning = project.getContentRootVirtualFiles().any { vf ->
val key = ScanInProgressKey(vf, ProductType.CODE_SECURITY)
ScanState.scanInProgress[key] == true
}

return (
AnalysisData.instance.isUpdateAnalysisInProgress(project) ||
RunUtils.instance.isFullRescanRequested(project) ||
lsRunning
)
}

fun isIacRunning(project: Project): Boolean {
val indicator = getSnykTaskQueueService(project)?.iacScanProgressIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,10 @@ class SnykTaskQueueService(val project: Project) {

fun connectProjectToLanguageServer(project: Project) {
synchronized(LanguageServerWrapper) {
val languageServerWrapper = LanguageServerWrapper.getInstance()
if (!languageServerWrapper.isInitialized && !languageServerWrapper.isInitializing) {
languageServerWrapper.initialize()
val added = languageServerWrapper.getWorkspaceFolders(project)
languageServerWrapper.updateWorkspaceFolders(added, emptySet())
}
val wrapper = LanguageServerWrapper.getInstance()
wrapper.ensureLanguageServerInitialized()
val added = wrapper.getWorkspaceFolders(project)
wrapper.updateWorkspaceFolders(added, emptySet())
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package snyk.common.lsp

import com.intellij.ide.impl.ProjectUtil
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
Expand Down Expand Up @@ -207,9 +208,20 @@ class LanguageServerWrapper(
}

fun sendScanCommand() {
ensureLanguageServerInitialized()
val project = ProjectUtil.getActiveProject()
if (project == null) {
logger.warn("No active project found, not sending scan command.")
return
}
project.getContentRootVirtualFiles().mapNotNull { it.path }.toSet().forEach(::sendFolderScanCommand)
}

private fun sendFolderScanCommand(folder: String) {
try {
val param = ExecuteCommandParams()
param.command = "snyk.workspace.scan"
param.command = "snyk.workspaceFolder.scan"
param.arguments = listOf(folder)
languageServer.workspaceService.executeCommand(param)
} catch (ignored: Exception) {
// do nothing to not break UX for analytics
Expand Down
7 changes: 5 additions & 2 deletions src/main/kotlin/snyk/common/lsp/ScanState.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package snyk.common.lsp

import com.intellij.openapi.vfs.VirtualFile
import snyk.common.ProductType
import java.util.Collections

object ScanState {
const val SNYK_CODE = "code"
val scanInProgress: MutableMap<String, Boolean> = Collections.synchronizedMap(mutableMapOf())
val scanInProgress: MutableMap<ScanInProgressKey, Boolean> = Collections.synchronizedMap(mutableMapOf())
}

data class ScanInProgressKey(val folderPath: VirtualFile, val productType: ProductType)
61 changes: 40 additions & 21 deletions src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import org.eclipse.lsp4j.WorkDoneProgressKind.report
import org.eclipse.lsp4j.WorkDoneProgressReport
import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
import org.eclipse.lsp4j.services.LanguageClient
import snyk.common.ProductType
import snyk.common.SnykCodeFileIssueComparator
import snyk.trust.WorkspaceTrustService
import java.util.Collections
Expand Down Expand Up @@ -83,39 +84,55 @@ class SnykLanguageClient : LanguageClient {
}

override fun refreshInlineValues(): CompletableFuture<Void> {
val activeProject = ProjectUtil.getActiveProject() ?: return CompletableFuture.completedFuture(null)
val completedFuture: CompletableFuture<Void> = CompletableFuture.completedFuture(null)
if (!isSnykCodeLSEnabled()) return completedFuture
val activeProject = ProjectUtil.getActiveProject() ?: return completedFuture

DaemonCodeAnalyzer.getInstance(activeProject).restart()
return CompletableFuture.completedFuture(null)
return completedFuture
}

@JsonNotification(value = "$/snyk.scan")
fun snykScan(snykScan: SnykScanParams) {
if (snykScan.product != ScanState.SNYK_CODE || !isSnykCodeLSEnabled()) return
if (!isSnykCodeLSEnabled()) return
try {
getScanPublishersFor(snykScan).forEach { (project, scanPublisher) ->
when (snykScan.status) {
"inProgress" -> {
if (ScanState.scanInProgress[snykScan.product] == true) return
ScanState.scanInProgress[snykScan.product] = true
scanPublisher.scanningStarted(snykScan)
}

"success" -> {
ScanState.scanInProgress[snykScan.product] = false
processSuccessfulScan(snykScan, scanPublisher, project)
}

"error" -> {
ScanState.scanInProgress[snykScan.product] = false
scanPublisher.scanningSnykCodeError(snykScan)
}
}
processSnykScan(snykScan, scanPublisher, project)
}
} catch (e: Exception) {
logger.error("Error processing snyk scan", e)
}
}

private fun processSnykScan(
snykScan: SnykScanParams,
scanPublisher: SnykCodeScanListenerLS,
project: Project
) {
val product = when (snykScan.product) {
"code" -> ProductType.CODE_SECURITY
else -> return
}
val key = ScanInProgressKey(snykScan.folderPath.toVirtualFile(), product)
when (snykScan.status) {
"inProgress" -> {
if (ScanState.scanInProgress[key] == true) return
ScanState.scanInProgress[key] = true
scanPublisher.scanningStarted(snykScan)
}

"success" -> {
ScanState.scanInProgress[key] = false
processSuccessfulScan(snykScan, scanPublisher, project)
}

"error" -> {
ScanState.scanInProgress[key] = false
scanPublisher.scanningSnykCodeError(snykScan)
}
}
}

private fun processSuccessfulScan(
snykScan: SnykScanParams,
scanPublisher: SnykCodeScanListenerLS,
Expand Down Expand Up @@ -151,11 +168,13 @@ class SnykLanguageClient : LanguageClient {
}.toSet()
}

@Suppress("UselessCallOnNotNull") // because lsp4j doesn't care about Kotlin non-null safety
private fun getSnykCodeResult(project: Project, snykScan: SnykScanParams): Map<SnykCodeFile, List<ScanIssue>> {
check(snykScan.product == "code") { "Expected Snyk Code scan result" }
if (snykScan.issues.isNullOrEmpty()) return emptyMap()
val map = snykScan.issues
.groupBy { it.filePath }
.map { (file, issues) -> SnykCodeFile(project, file.toVirtualFile()) to issues.sorted() }
.mapNotNull { (file, issues) -> SnykCodeFile(project, file.toVirtualFile()) to issues.sorted() }
.filter { it.second.isNotEmpty() }
.toMap()
return map.toSortedMap(SnykCodeFileIssueComparator(map))
Expand Down
Loading