[qodana] QD-8216 Implement *.inspection.kts inspections

- provide inspections as a `Flow` via `DynamicInspectionsProvider` EP

GitOrigin-RevId: 59fdb6af5583f606e060a80319881d15604fd02a
This commit is contained in:
Mikhail Shagvaliev
2023-12-17 14:49:09 +01:00
committed by intellij-monorepo-bot
parent a4f4693783
commit 58b4d1ba37
6 changed files with 140 additions and 3 deletions

View File

@@ -33,6 +33,7 @@
</extensionPoint>
<extensionPoint name="inspectionToolProvider" interface="com.intellij.codeInspection.InspectionToolProvider" dynamic="true"/>
<extensionPoint name="dynamicInspectionsProvider" interface="com.intellij.codeInspection.ex.DynamicInspectionsProvider" dynamic="true"/>
<extensionPoint name="codeInspection.InspectionExtension" interface="com.intellij.codeInspection.lang.InspectionExtensionsFactory" dynamic="true"/>
<extensionPoint name="inspectionsReportConverter" interface="com.intellij.codeInspection.InspectionsReportConverter" dynamic="true"/>

View File

@@ -330,6 +330,10 @@ public abstract class InspectionProfileEntry implements BatchSuppressableTool, O
return getShortName(getClass().getSimpleName());
}
public @Nullable String getLanguage() {
return null;
}
public static @NotNull String getShortName(@NotNull String className) {
return StringUtil.trimEnd(StringUtil.trimEnd(className, "Inspection"), "InspectionBase");
}

View File

@@ -97,7 +97,7 @@ public abstract class InspectionToolWrapper<T extends InspectionProfileEntry, E
* @see #isApplicable(Language)
*/
public @Nullable String getLanguage() {
return myEP == null ? null : myEP.language;
return myEP == null ? myTool.getLanguage() : myEP.language;
}
public boolean applyToDialects() {

View File

@@ -0,0 +1,53 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.ex
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@Service(Service.Level.PROJECT)
class ProjectInspectionToolRegistrar(private val project: Project, scope: CoroutineScope) : InspectionToolsSupplier() {
companion object {
fun getInstance(project: Project): ProjectInspectionToolRegistrar = project.service()
}
private val dynamicInspectionsFlow: StateFlow<Set<DynamicInspectionDescriptor>> = dynamicInspectionsFlow(project)
.flowOn(Dispatchers.Default)
.stateIn(scope, SharingStarted.Eagerly, initialValue = emptySet())
init {
InspectionToolRegistrar.getInstance()
scope.launch(Dispatchers.Default) {
var oldInspections = emptySet<DynamicInspectionDescriptor>()
dynamicInspectionsFlow.collectLatest { currentInspections ->
if (oldInspections == currentInspections) return@collectLatest
val newInspections = currentInspections - oldInspections
val outdatedInspections = oldInspections - currentInspections
listeners.forEach { listener ->
outdatedInspections.forEach {
listener.toolRemoved(it.toolWrapper)
}
}
listeners.forEach { listener ->
newInspections.forEach {
listener.toolAdded(it.toolWrapper)
}
}
oldInspections = currentInspections
DaemonCodeAnalyzerEx.getInstance(project).restart()
}
}
}
override fun createTools(): MutableList<InspectionToolWrapper<*, *>> {
return (InspectionToolRegistrar.getInstance().createTools() + dynamicInspectionsFlow.value.map { it.toolWrapper }).toMutableList()
}
}

View File

@@ -0,0 +1,78 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection.ex
import com.intellij.codeInspection.GlobalInspectionTool
import com.intellij.codeInspection.InspectionProfileEntry
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.openapi.extensions.ExtensionPointListener
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.*
private val EP_NAME = ExtensionPointName<DynamicInspectionsProvider>("com.intellij.dynamicInspectionsProvider")
interface DynamicInspectionsProvider {
fun inspectionsFlow(project: Project): Flow<Set<DynamicInspectionDescriptor>>
}
sealed class DynamicInspectionDescriptor {
companion object {
fun fromTool(tool: InspectionProfileEntry): DynamicInspectionDescriptor {
return when(tool) {
is LocalInspectionTool -> Local(tool)
is GlobalInspectionTool -> Global(tool)
else -> error("Got ${tool}, expected ${LocalInspectionTool::class.java} or ${GlobalInspectionTool::class.java}")
}
}
}
val toolWrapper: InspectionToolWrapper<*, *> by lazy {
when(this) {
is Global -> GlobalInspectionToolWrapper(tool)
is Local -> LocalInspectionToolWrapper(tool)
}
}
class Global(val tool: GlobalInspectionTool) : DynamicInspectionDescriptor()
class Local(val tool: LocalInspectionTool) : DynamicInspectionDescriptor()
}
@OptIn(ExperimentalCoroutinesApi::class)
internal fun dynamicInspectionsFlow(project: Project): Flow<Set<DynamicInspectionDescriptor>> {
val epUpdatedFlow = callbackFlow {
trySendBlocking(Unit)
val disposable = Disposer.newDisposable()
val listener = object : ExtensionPointListener<DynamicInspectionsProvider> {
override fun extensionAdded(extension: DynamicInspectionsProvider, pluginDescriptor: PluginDescriptor) {
trySendBlocking(Unit)
}
override fun extensionRemoved(extension: DynamicInspectionsProvider, pluginDescriptor: PluginDescriptor) {
trySendBlocking(Unit)
}
}
EP_NAME.addExtensionPointListener(listener, disposable)
awaitClose { Disposer.dispose(disposable) }
}.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_LATEST)
return epUpdatedFlow
.flatMapLatest {
val allDynamicCustomInspectionsFlows: List<Flow<Set<DynamicInspectionDescriptor>>> = EP_NAME.extensionList.map { provider ->
provider.inspectionsFlow(project)
.map { inspections -> inspections.toSet() }
.onStart { emit(emptySet()) }
}
combine(allDynamicCustomInspectionsFlows) {
it.toList().flatten().toSet()
}
}.distinctUntilChanged()
}

View File

@@ -3,6 +3,7 @@ package com.intellij.profile.codeInspection
import com.intellij.codeInspection.ex.InspectionProfileImpl
import com.intellij.codeInspection.ex.InspectionToolRegistrar
import com.intellij.codeInspection.ex.ProjectInspectionToolRegistrar
import com.intellij.configurationStore.*
import com.intellij.diagnostic.runActivity
import com.intellij.openapi.Disposable
@@ -59,7 +60,7 @@ open class ProjectInspectionProfileManager(final override val project: Project)
name: String,
attributeProvider: (String) -> String?,
isBundled: Boolean): InspectionProfileImpl {
val profile = InspectionProfileImpl(name, InspectionToolRegistrar.getInstance(), this@ProjectInspectionProfileManager, dataHolder)
val profile = InspectionProfileImpl(name, ProjectInspectionToolRegistrar.getInstance(project), this@ProjectInspectionProfileManager, dataHolder)
profile.isProjectLevel = true
return profile
}
@@ -239,7 +240,7 @@ open class ProjectInspectionProfileManager(final override val project: Project)
if (currentScheme == null) {
currentScheme = schemeManager.allSchemes.firstOrNull()
if (currentScheme == null) {
currentScheme = InspectionProfileImpl(PROJECT_DEFAULT_PROFILE_NAME, InspectionToolRegistrar.getInstance(), this)
currentScheme = InspectionProfileImpl(PROJECT_DEFAULT_PROFILE_NAME, ProjectInspectionToolRegistrar.getInstance(project), this)
currentScheme.copyFrom(InspectionProfileManager.getInstance().currentProfile)
currentScheme.isProjectLevel = true
currentScheme.name = PROJECT_DEFAULT_PROFILE_NAME