Skip to content

Commit

Permalink
chore: improve annotator performance
Browse files Browse the repository at this point in the history
call language server for code actions only once per range, do pre-sort before UI thread
  • Loading branch information
bastiandoetsch committed Sep 7, 2024
1 parent 6a583e6 commit e9b4af2
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 60 deletions.
9 changes: 0 additions & 9 deletions src/main/kotlin/io/snyk/plugin/Severity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,6 @@ enum class Severity {
private const val SEVERITY_MEDIUM = "medium"
private const val SEVERITY_LOW = "low"

fun getFromIndex(index: Int): Severity =
when (index) {
4 -> CRITICAL
3 -> HIGH
2 -> MEDIUM
1 -> LOW
else -> UNKNOWN
}

fun getFromName(name: String): Severity =
when (name) {
SEVERITY_CRITICAL -> CRITICAL
Expand Down
141 changes: 90 additions & 51 deletions src/main/kotlin/snyk/common/annotator/SnykAnnotator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@ import com.intellij.openapi.editor.markup.GutterIconRenderer
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.guessProjectForFile
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import icons.SnykIcons
import io.snyk.plugin.Severity
import io.snyk.plugin.getSnykCachedResultsForProduct
import io.snyk.plugin.getSnykToolWindowPanel
import io.snyk.plugin.toLanguageServerURL
import org.eclipse.lsp4j.CodeAction
import org.eclipse.lsp4j.CodeActionContext
import org.eclipse.lsp4j.CodeActionParams
import org.eclipse.lsp4j.Command
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.eclipse.lsp4j.jsonrpc.messages.Either
import org.jetbrains.concurrency.runAsync
import snyk.common.AnnotatorCommon
import snyk.common.ProductType
Expand All @@ -46,7 +50,8 @@ import javax.swing.Icon
private const val CODEACTION_TIMEOUT = 5000L

abstract class SnykAnnotator(private val product: ProductType) :
ExternalAnnotator<Pair<PsiFile, List<ScanIssue>>, List<SnykAnnotation>>(), Disposable, DumbAware {
ExternalAnnotator<Pair<PsiFile, Map<Range, List<ScanIssue>>>, List<SnykAnnotation>>(), Disposable, DumbAware {
private val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider()

val logger = logger<SnykAnnotator>()
protected var disposed = false
Expand All @@ -70,53 +75,77 @@ abstract class SnykAnnotator(private val product: ProductType) :
)

// overrides needed for the Annotator to invoke apply(). We don't do anything here
override fun collectInformation(file: PsiFile): Pair<PsiFile, List<ScanIssue>> {
return Pair(file, getIssuesForFile(file)
override fun collectInformation(file: PsiFile): Pair<PsiFile, Map<Range, List<ScanIssue>>>? {
val map = getIssuesForFile(file)
.filter { AnnotatorCommon.isSeverityToShow(it.getSeverityAsEnum()) }
.distinctBy { it.id })
.sortedByDescending { it.getSeverityAsEnum() }
.groupBy { it.range }
.toMap()

return Pair(file, map)
}

override fun doAnnotate(initialInfo: Pair<PsiFile, List<ScanIssue>>): List<SnykAnnotation> {
override fun doAnnotate(initialInfo: Pair<PsiFile, Map<Range, List<ScanIssue>>>): List<SnykAnnotation> {
if (disposed) return emptyList()
AnnotatorCommon.prepareAnnotate(initialInfo.first)
if (!LanguageServerWrapper.getInstance().isInitialized) return emptyList()

val lineMarkerProviderDescriptor: SnykLineMarkerProvider = getLineMarkerProvider()
val gutterIconEnabled = LineMarkerSettings.getSettings().isEnabled(lineMarkerProviderDescriptor)

val codeActions = initialInfo.second
.map { entry ->
entry.key to getCodeActions(initialInfo.first.virtualFile, entry.key).map { it.right }
.sortedBy { it.title }
}.toMap()

val annotations = mutableListOf<SnykAnnotation>()
val gutterIcons: MutableSet<TextRange> = mutableSetOf()
initialInfo.second.forEach { entry ->
val textRange = textRange(initialInfo.first, entry.key)
if (textRange == null || textRange.isEmpty) {
logger.warn("Invalid range for range: $textRange")
return@forEach
}
annotations.addAll(
doAnnotateIssue(entry, textRange, gutterIconEnabled, codeActions)
)
}
return annotations.sortedByDescending { it.issue.getSeverityAsEnum() }
}

initialInfo.second.sortedByDescending { it.getSeverityAsEnum() }.forEach { issue ->
val textRange = textRange(initialInfo.first, issue.range)
private fun doAnnotateIssue(
entry: Map.Entry<Range, List<ScanIssue>>,
textRange: TextRange,
gutterIconEnabled: Boolean,
codeActions: Map<Range, List<CodeAction>>,
): List<SnykAnnotation> {
val gutterIcons: MutableSet<TextRange> = mutableSetOf()
val annotations = mutableListOf<SnykAnnotation>()
entry.value.forEach { issue ->
val highlightSeverity = issue.getSeverityAsEnum().getHighlightSeverity()
val annotationMessage = issue.annotationMessage()
if (textRange == null) {
logger.warn("Invalid range for issue: $issue")
return@forEach
}
if (!textRange.isEmpty) {
val detailAnnotation = SnykAnnotation(
issue,
highlightSeverity,
annotationMessage,
textRange,
)

val gutterIconRenderer =
if (gutterIconEnabled && !gutterIcons.contains(textRange)) {
gutterIcons.add(textRange)
SnykShowDetailsGutterRenderer(detailAnnotation)
} else {
null
}

detailAnnotation.gutterIconRenderer = gutterIconRenderer
annotations.add(detailAnnotation)
val detailAnnotation = SnykAnnotation(
issue,
highlightSeverity,
annotationMessage,
textRange,
)

detailAnnotation.intentionActions.add(ShowDetailsIntentionAction(annotationMessage, issue))
detailAnnotation.intentionActions.addAll(getCodeActionsAsIntentionActions(initialInfo, issue))
}
val gutterIconRenderer =
if (gutterIconEnabled && !gutterIcons.contains(textRange)) {
gutterIcons.add(textRange)
SnykShowDetailsGutterRenderer(detailAnnotation)
} else {
null
}

val languageServerIntentionActions = codeActions[entry.key]?.let { range ->
getCodeActionsAsIntentionActions(issue, range)
} ?: emptyList()

detailAnnotation.gutterIconRenderer = gutterIconRenderer
detailAnnotation.intentionActions.add(ShowDetailsIntentionAction(annotationMessage, issue))
detailAnnotation.intentionActions.addAll(languageServerIntentionActions)
annotations.add(detailAnnotation)
}
return annotations
}
Expand All @@ -129,9 +158,11 @@ abstract class SnykAnnotator(private val product: ProductType) :
if (disposed) return
if (!LanguageServerWrapper.getInstance().isInitialized) return
annotationResult
.sortedByDescending { it.issue.getSeverityAsEnum() }
.forEach { annotation ->
if (!annotation.range.isEmpty) {
val annoBuilder = holder.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage)
val annoBuilder = holder
.newAnnotation(annotation.annotationSeverity, annotation.annotationMessage)
.range(annotation.range)
.textAttributes(getTextAttributeKeyBySeverity(annotation.issue.getSeverityAsEnum()))

Expand All @@ -142,20 +173,39 @@ abstract class SnykAnnotator(private val product: ProductType) :
if (annotation.gutterIconRenderer != null) {
annoBuilder.gutterIconRenderer(SnykShowDetailsGutterRenderer(annotation))
}

annoBuilder.create()
}
}
}

private fun getCodeActionsAsIntentionActions(
initial: Pair<PsiFile, List<ScanIssue>>,
issue: ScanIssue,
codeActions: List<CodeAction>
): MutableList<IntentionAction> {
val addedIntentionActions = mutableListOf<IntentionAction>()

codeActions
.filter { action ->
val diagnosticCode = action.diagnostics?.get(0)?.code?.left
val ruleId = issue.ruleId()
diagnosticCode == ruleId
}
.forEach { action ->
addedIntentionActions.add(CodeActionIntention(issue, action, product))
}

return addedIntentionActions
}

private fun getCodeActions(
file: VirtualFile,
range: Range
): List<Either<Command, CodeAction>> {
val params =
CodeActionParams(
TextDocumentIdentifier(initial.first.virtualFile.toLanguageServerURL()),
issue.range,
TextDocumentIdentifier(file.toLanguageServerURL()),
range,
CodeActionContext(emptyList()),
)
val languageServer = LanguageServerWrapper.getInstance().languageServer
Expand All @@ -164,21 +214,10 @@ abstract class SnykAnnotator(private val product: ProductType) :
languageServer.textDocumentService
.codeAction(params).get(CODEACTION_TIMEOUT, TimeUnit.MILLISECONDS) ?: emptyList()
} catch (ignored: TimeoutException) {
logger.info("Timeout fetching code actions for issue: $issue")
logger.info("Timeout fetching code actions for range: $range")
emptyList()
}

codeActions
.filter { a ->
val diagnosticCode = a.right.diagnostics?.get(0)?.code?.left
val ruleId = issue.ruleId()
diagnosticCode == ruleId
}
.sortedBy { it.right.title }.forEach { action ->
val codeAction = action.right
addedIntentionActions.add(CodeActionIntention(issue, codeAction, product))
}
return addedIntentionActions
return codeActions
}

private fun getLineMarkerProvider(): SnykLineMarkerProvider {
Expand Down

0 comments on commit e9b4af2

Please sign in to comment.