1- @file:Suppress(" UnstableApiUsage" )
2-
31/*
42 * Copyright (c) 2026 ghostflyby
53 * SPDX-FileCopyrightText: 2026 ghostflyby
@@ -27,14 +25,9 @@ package dev.ghostflyby.mcp.quality
2725import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor
2826import com.intellij.codeInsight.actions.OptimizeImportsProcessor
2927import com.intellij.codeInsight.actions.ReformatCodeProcessor
30- import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
31- import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl
32- import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator
33- import com.intellij.codeInsight.daemon.impl.HighlightInfo
34- import com.intellij.codeInsight.daemon.impl.HighlightingSessionImpl
35- import com.intellij.codeInsight.multiverse.defaultContext
36- import com.intellij.codeInspection.InspectionProfile
28+ import com.intellij.codeInspection.*
3729import com.intellij.codeInspection.ex.GlobalInspectionContextBase
30+ import com.intellij.codeInspection.ex.InspectionToolWrapper
3831import com.intellij.lang.annotation.HighlightSeverity
3932import com.intellij.mcpserver.McpToolset
4033import com.intellij.mcpserver.annotations.McpDescription
@@ -48,36 +41,19 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
4841import com.intellij.openapi.progress.ProgressManager
4942import com.intellij.openapi.roots.ContentIterator
5043import com.intellij.openapi.roots.ProjectRootManager
51- import com.intellij.openapi.util.ProperTextRange
5244import com.intellij.openapi.util.TextRange
5345import com.intellij.openapi.vfs.VirtualFile
5446import com.intellij.platform.ide.progress.withBackgroundProgress
55- import com.intellij.psi.PsiFile
56- import com.intellij.psi.PsiManager
5747import com.intellij.profile.codeInspection.InspectionProfileManager
5848import com.intellij.profile.codeInspection.InspectionProjectProfileManager
49+ import com.intellij.psi.PsiErrorElement
50+ import com.intellij.psi.PsiFile
51+ import com.intellij.psi.PsiManager
52+ import com.intellij.psi.util.PsiTreeUtil
5953import dev.ghostflyby.mcp.Bundle
60- import dev.ghostflyby.mcp.common.ALLOW_UI_INTERACTIVE_SCOPES_PARAM_DESCRIPTION
61- import dev.ghostflyby.mcp.common.AGENT_FIRST_CALL_SHORTCUT_DESCRIPTION_SUFFIX
62- import dev.ghostflyby.mcp.common.VFS_URL_PARAM_DESCRIPTION
63- import dev.ghostflyby.mcp.common.batchTry
64- import dev.ghostflyby.mcp.common.findFileByUrlWithRefresh
65- import dev.ghostflyby.mcp.common.relativizePathOrNull
66- import dev.ghostflyby.mcp.common.reportActivity
67- import dev.ghostflyby.mcp.scope.ScopeProgramDescriptorDto
68- import dev.ghostflyby.mcp.scope.ScopeQuickPreset
69- import dev.ghostflyby.mcp.scope.ScopeResolverService
70- import dev.ghostflyby.mcp.scope.ScopeShape
71- import dev.ghostflyby.mcp.scope.buildPresetScopeDescriptor
72- import kotlinx.coroutines.CompletableDeferred
73- import kotlinx.coroutines.Dispatchers
74- import kotlinx.coroutines.coroutineScope
75- import kotlinx.coroutines.currentCoroutineContext
76- import kotlinx.coroutines.delay
77- import kotlinx.coroutines.isActive
78- import kotlinx.coroutines.launch
79- import kotlinx.coroutines.withContext
80- import kotlinx.coroutines.withTimeoutOrNull
54+ import dev.ghostflyby.mcp.common.*
55+ import dev.ghostflyby.mcp.scope.*
56+ import kotlinx.coroutines.*
8157import kotlinx.serialization.EncodeDefault
8258import kotlinx.serialization.ExperimentalSerializationApi
8359import kotlinx.serialization.Serializable
@@ -1160,51 +1136,161 @@ internal class CodeQualityMcpTools : McpToolset {
11601136 file : VirtualFile ,
11611137 errorsOnly : Boolean ,
11621138 ): List <QualityProblemDto > {
1163- val minSeverity = if (errorsOnly) HighlightSeverity .ERROR else HighlightSeverity .WEAK_WARNING
1164- return readAction {
1139+ val minSeverity = if (errorsOnly) HighlightSeverity .ERROR else HighlightSeverity .WARNING
1140+ val psiFile = readAction {
11651141 if (file.isDirectory) {
11661142 mcpFail(" URL points to a directory, not a file: ${file.url} " )
11671143 }
11681144 if (file.fileType.isBinary) {
11691145 mcpFail(" File at URL '${file.url} ' is binary and cannot be analyzed." )
11701146 }
1171- val psiFile = PsiManager .getInstance(project).findFile(file)
1147+ PsiManager .getInstance(project).findFile(file)
11721148 ? : mcpFail(" No PSI file available for URL: ${file.url} " )
1173- val document = FileDocumentManager .getInstance().getDocument(file)
1149+ }
1150+ val document = readAction {
1151+ FileDocumentManager .getInstance().getDocument(file)
11741152 ? : mcpFail(" No text document available for URL: ${file.url} " )
1175- val indicator = DaemonProgressIndicator ()
1176- val range = ProperTextRange (0 , document.textLength)
1177- val analyzer = DaemonCodeAnalyzer .getInstance(project) as ? DaemonCodeAnalyzerImpl
1178- ? : mcpFail(" Daemon code analyzer implementation is unavailable for this project." )
1179- val problems = mutableListOf<QualityProblemDto >()
1180- HighlightingSessionImpl .runInsideHighlightingSession(psiFile, defaultContext(), null , range, false ) { session ->
1181- (session as HighlightingSessionImpl ).setMinimumSeverity(minSeverity)
1182- val highlights = analyzer.runMainPasses(psiFile, document, indicator)
1183- highlights.forEach { info ->
1184- if (info.severity.myVal >= minSeverity.myVal) {
1185- problems + = createQualityProblem(document, info)
1186- }
1187- }
1153+ }
1154+ val profile = resolveInspectionProfile(project, null )
1155+ val inspectionManager = InspectionManager .getInstance(project)
1156+ val tools = readAction { collectEnabledInspectionTools(profile, psiFile) }
1157+ val toolSeverityByShortName = readAction {
1158+ tools.associate { wrapper ->
1159+ wrapper.shortName to resolveToolSeverity(profile, psiFile, wrapper)
11881160 }
1189- problems
11901161 }
1162+
1163+ val deduplicated = linkedMapOf<String , QualityProblemDto >()
1164+
1165+ tools.forEach { tool ->
1166+ ProgressManager .checkCanceled()
1167+ val descriptors = InspectionEngine .runInspectionOnFile(
1168+ psiFile,
1169+ tool,
1170+ inspectionManager.createNewGlobalContext(),
1171+ )
1172+ descriptors.forEach { descriptor ->
1173+ val severity = resolveProblemSeverity(
1174+ descriptor,
1175+ toolSeverityByShortName[tool.shortName] ? : HighlightSeverity .WARNING ,
1176+ )
1177+ if (severity.myVal < minSeverity.myVal) return @forEach
1178+ val problem = buildProblemFromDescriptor(document, descriptor, severity)
1179+ ? : return @forEach
1180+ deduplicated.putIfAbsent(
1181+ " ${problem.severity} :${problem.line} :${problem.column} :${problem.description} " ,
1182+ problem,
1183+ )
1184+ }
1185+ }
1186+
1187+ val syntaxErrors = readAction { PsiTreeUtil .findChildrenOfType(psiFile, PsiErrorElement ::class .java) }
1188+ syntaxErrors.forEach { error ->
1189+ val severity = HighlightSeverity .ERROR
1190+ if (severity.myVal < minSeverity.myVal) return @forEach
1191+ val problem = createQualityProblem(
1192+ document = document,
1193+ severity = severity.name,
1194+ description = error.errorDescription,
1195+ startOffset = error.textRange.startOffset,
1196+ )
1197+ deduplicated.putIfAbsent(
1198+ " ${problem.severity} :${problem.line} :${problem.column} :${problem.description} " ,
1199+ problem,
1200+ )
1201+ }
1202+
1203+ return deduplicated.values.toList()
11911204 }
11921205
1193- private fun createQualityProblem (document : Document , info : HighlightInfo ): QualityProblemDto {
1194- val startLine = document.getLineNumber(info.startOffset)
1206+ private fun createQualityProblem (
1207+ document : Document ,
1208+ severity : String ,
1209+ description : String? ,
1210+ startOffset : Int ,
1211+ ): QualityProblemDto {
1212+ val boundedOffset = startOffset.coerceIn(0 , document.textLength)
1213+ val startLine = document.getLineNumber(boundedOffset)
11951214 val lineStartOffset = document.getLineStartOffset(startLine)
11961215 val lineEndOffset = document.getLineEndOffset(startLine)
11971216 val lineContent = document.getText(TextRange (lineStartOffset, lineEndOffset))
1198- val column = info.startOffset - lineStartOffset
1217+ val column = boundedOffset - lineStartOffset
11991218 return QualityProblemDto (
1200- severity = info. severity.name ,
1201- description = info. description,
1219+ severity = severity,
1220+ description = description,
12021221 lineContent = lineContent,
12031222 line = startLine + 1 ,
12041223 column = column + 1 ,
12051224 )
12061225 }
12071226
1227+ private fun collectEnabledInspectionTools (
1228+ profile : InspectionProfile ,
1229+ psiFile : PsiFile ,
1230+ ): List <InspectionToolWrapper <* , * >> {
1231+ return profile.getInspectionTools(psiFile)
1232+ .asSequence()
1233+ .filter { tool ->
1234+ ProgressManager .checkCanceled()
1235+ val key = tool.displayKey ? : return @filter false
1236+ profile.isToolEnabled(key, psiFile)
1237+ }
1238+ .toList()
1239+ }
1240+
1241+ private fun resolveToolSeverity (
1242+ profile : InspectionProfile ,
1243+ psiFile : PsiFile ,
1244+ tool : InspectionToolWrapper <* , * >,
1245+ ): HighlightSeverity {
1246+ val level = tool.displayKey?.let { key ->
1247+ profile.getErrorLevel(key, psiFile)
1248+ } ? : tool.defaultLevel
1249+ return level.severity
1250+ }
1251+
1252+ private fun resolveProblemSeverity (
1253+ descriptor : ProblemDescriptor ,
1254+ configuredToolSeverity : HighlightSeverity ,
1255+ ): HighlightSeverity {
1256+ return when (descriptor.highlightType) {
1257+ ProblemHighlightType .ERROR ,
1258+ ProblemHighlightType .GENERIC_ERROR ,
1259+ -> HighlightSeverity .ERROR
1260+
1261+ ProblemHighlightType .WARNING -> HighlightSeverity .WARNING
1262+ ProblemHighlightType .WEAK_WARNING -> HighlightSeverity .WEAK_WARNING
1263+ ProblemHighlightType .INFORMATION ,
1264+ -> HighlightSeverity .INFORMATION
1265+
1266+ else -> configuredToolSeverity
1267+ }
1268+ }
1269+
1270+ private suspend fun buildProblemFromDescriptor (
1271+ document : Document ,
1272+ descriptor : ProblemDescriptor ,
1273+ severity : HighlightSeverity ,
1274+ ): QualityProblemDto ? {
1275+ val snapshot = readAction {
1276+ val anchor = descriptor.startElement ? : descriptor.psiElement ? : return @readAction null
1277+ val anchorRange = anchor.textRange ? : return @readAction null
1278+ val rangeInElement = descriptor.textRangeInElement
1279+ val startOffset = anchorRange.startOffset + rangeInElement.startOffset
1280+ val description = ProblemDescriptorUtil .renderDescriptionMessage(descriptor, anchor)
1281+ DescriptorProblemSnapshot (
1282+ startOffset = startOffset,
1283+ description = description,
1284+ )
1285+ } ? : return null
1286+ return createQualityProblem(
1287+ document = document,
1288+ severity = severity.name,
1289+ description = snapshot.description,
1290+ startOffset = snapshot.startOffset,
1291+ )
1292+ }
1293+
12081294 private suspend fun collectProjectContentFilesInScope (
12091295 project : com.intellij.openapi.project.Project ,
12101296 resolvedScope : com.intellij.psi.search.SearchScope ,
@@ -1343,6 +1429,11 @@ internal class CodeQualityMcpTools : McpToolset {
13431429 return relativizePathOrNull(projectBasePath, filePath)
13441430 }
13451431
1432+ private data class DescriptorProblemSnapshot (
1433+ val startOffset : Int ,
1434+ val description : String ,
1435+ )
1436+
13461437 private data class ScopeFileCollection (
13471438 val files : List <VirtualFile >,
13481439 val scannedFileCount : Int ,
0 commit comments