[workspace model] IJPL-201515 Workspace Inspections

- split workspace inspections into K1 and K2
- tests
- include tests in Kotlin K2 tests


Merge-request: IJ-MR-162676
Merged-by: Kirill Bochkarev <kirill.bochkarev@jetbrains.com>

GitOrigin-RevId: e161e7c95e330fa478ece69e4c252981349e97bb
This commit is contained in:
Kirill Bochkarev
2025-10-27 17:52:33 +00:00
committed by intellij-monorepo-bot
parent 23fd6c469f
commit 738a0877e3
48 changed files with 1083 additions and 293 deletions
@@ -12,7 +12,7 @@ import java.util.stream.Stream;
public class DevkitInspectionsRegistrationCheckTest extends BasePlatformTestCase {
private static final int EXPECTED_INSPECTIONS_NUMBER = 88;
private static final int EXPECTED_INSPECTIONS_NUMBER = 87;
/**
* Inspections that are finished and intentionally disabled.
@@ -24,7 +24,7 @@ public class DevkitInspectionsRegistrationCheckTest extends BasePlatformTestCase
).sorted().toList();
/**
* Inspections which implementation is in
* Inspections which implementation is in progress
* or are finished but not battle-tested yet and may require improvements/polishing.
*/
private static final List<String> WIP_INSPECTIONS =
@@ -66,6 +66,7 @@ jvm_library(
"//platform/core-api:core",
"//platform/workspace/jps",
"//java/java-impl:impl",
"//plugins/kotlin/base/psi",
]
)
### auto-generated section `build intellij.devkit.workspaceModel` end
@@ -63,5 +63,6 @@
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.workspace.jps" />
<orderEntry type="module" module-name="intellij.java.impl" />
<orderEntry type="module" module-name="intellij.kotlin.base.psi" />
</component>
</module>
@@ -39,6 +39,7 @@ jvm_library(
"//java/java-psi-impl:psi-impl",
"//platform/util",
"//platform/core-api:core",
"//platform/analysis-api:analysis",
]
)
@@ -72,6 +73,7 @@ jvm_library(
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//plugins/kotlin/base/test:test_test_lib",
"//platform/analysis-api:analysis",
]
)
### auto-generated section `build intellij.devkit.workspaceModel.k1` end
@@ -45,5 +45,6 @@
<orderEntry type="module" module-name="kotlin.tests-common" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="kotlin.base.test" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
</component>
</module>
@@ -5,9 +5,28 @@
<module name="intellij.devkit.workspaceModel"/>
</dependencies>
<resource-bundle>messages.DevKitWorkspaceModelBundle</resource-bundle>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.devkit.workspaceModel.metaModel.WorkspaceMetaModelProvider"
serviceImplementation="com.intellij.devkit.workspaceModel.k1.metaModel.WorkspaceMetaModelProviderImpl"
/>
<localInspection language="kotlin"
shortName="WorkspaceImplAbsent"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.k1.inspections.WorkspaceCodeAbsentInspection"
key="inspection.workspace.impl.generation.display.name"/>
<localInspection language="kotlin"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.k1.inspections.WorkspaceImplObsoleteInspection"
key="inspection.workspace.obsolete.model.display.name"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,22 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceCodeAbsentInspectionBase
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
@VisibleForTesting
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceCodeAbsentInspection : WorkspaceCodeAbsentInspectionBase() {
override fun belongToSameModule(ktClass: KtClassOrObject, otherKtClass: KtClassOrObject): Boolean =
ktClass.moduleInfo == otherKtClass.moduleInfo
override fun getModuleSearchScope(ktClass: KtClassOrObject): GlobalSearchScope =
ktClass.moduleInfo.contentScope
}
@@ -0,0 +1,19 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceImplObsoleteInspectionBase
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
@VisibleForTesting
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceImplObsoleteInspection : WorkspaceImplObsoleteInspectionBase() {
override fun getModuleSearchScope(ktClass: KtClassOrObject): GlobalSearchScope =
ktClass.moduleInfo.contentScope
}
@@ -0,0 +1,14 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceCodeAbsentInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceCodeAbsentInspectionTest : WorkspaceCodeAbsentInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceCodeAbsentInspection())
}
}
@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceEntityMutableFieldInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceEntityMutableFieldInspectionTest : WorkspaceEntityMutableFieldInspectionBaseTest()
@@ -0,0 +1,14 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceImplObsoleteInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceImplObsoleteInspectionTest : WorkspaceImplObsoleteInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceImplObsoleteInspection())
}
}
@@ -0,0 +1,27 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k1.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceInheritanceInspection
import com.intellij.devkit.workspaceModel.inspections.WorkspaceInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceInheritanceInspectionTest : WorkspaceInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceInheritanceInspection())
}
fun testInheritance() {
doTest()
}
fun testNotWorkspaceAbstract() {
doTest()
}
fun testNotWorkspaceClasses() {
doTest()
}
}
@@ -41,6 +41,7 @@ jvm_library(
"//platform/core-impl",
"//java/openapi:java",
"//plugins/kotlin/base/psi",
"//platform/analysis-api:analysis",
]
)
@@ -75,6 +76,7 @@ jvm_library(
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//plugins/kotlin/base/test:test_test_lib",
"//platform/analysis-api:analysis",
]
)
### auto-generated section `build intellij.devkit.workspaceModel.k2` end
@@ -46,5 +46,6 @@
<orderEntry type="module" module-name="kotlin.plugin.k2" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="kotlin.base.test" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
</component>
</module>
@@ -5,9 +5,28 @@
<module name="intellij.devkit.workspaceModel"/>
</dependencies>
<resource-bundle>messages.DevKitWorkspaceModelBundle</resource-bundle>
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.devkit.workspaceModel.metaModel.WorkspaceMetaModelProvider"
serviceImplementation="com.intellij.devkit.workspaceModel.k2.metaModel.WorkspaceMetaModelProviderImpl"
/>
<localInspection language="kotlin"
shortName="WorkspaceImplAbsent"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.k2.inspections.WorkspaceCodeAbsentInspection"
key="inspection.workspace.impl.generation.display.name"/>
<localInspection language="kotlin"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.k2.inspections.WorkspaceImplObsoleteInspection"
key="inspection.workspace.obsolete.model.display.name"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,29 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceCodeAbsentInspectionBase
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.getModule
import org.jetbrains.kotlin.idea.base.projectStructure.getKaModule
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
@VisibleForTesting
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceCodeAbsentInspection : WorkspaceCodeAbsentInspectionBase() {
override fun belongToSameModule(ktClass: KtClassOrObject, otherKtClass: KtClassOrObject): Boolean =
analyze(ktClass) {
getModule(ktClass) == getModule(otherKtClass)
}
override fun getModuleSearchScope(ktClass: KtClassOrObject): GlobalSearchScope =
analyze(ktClass) {
getModule(ktClass).contentScope
}
}
@@ -0,0 +1,21 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceImplObsoleteInspectionBase
import com.intellij.openapi.util.IntellijInternalApi
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.kotlin.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.getModule
import org.jetbrains.kotlin.psi.KtClassOrObject
@VisibleForTesting
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceImplObsoleteInspection : WorkspaceImplObsoleteInspectionBase() {
override fun getModuleSearchScope(ktClass: KtClassOrObject): GlobalSearchScope =
analyze(ktClass) {
getModule(ktClass).contentScope
}
}
@@ -0,0 +1,14 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceCodeAbsentInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceCodeAbsentInspectionTest : WorkspaceCodeAbsentInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceCodeAbsentInspection())
}
}
@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceEntityMutableFieldInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceEntityMutableFieldInspectionTest : WorkspaceEntityMutableFieldInspectionBaseTest()
@@ -0,0 +1,14 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceImplObsoleteInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceImplObsoleteInspectionTest : WorkspaceImplObsoleteInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceImplObsoleteInspection())
}
}
@@ -0,0 +1,9 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.k2.inspections
import com.intellij.devkit.workspaceModel.inspections.WorkspaceInheritanceInspectionBaseTest
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceInheritanceInspectionTest : WorkspaceInheritanceInspectionBaseTest()
@@ -1,6 +1,6 @@
<html>
<body>
Reports existence of the obsolete implementation for the entity.
Reports the existence of the obsolete implementation for the entity.
<p>
Verifies that existing implementation for entities has the same API version as described at <code>com.intellij.platform.workspace.storage.CodeGeneratorVersions</code> from dependencies.
</p>
@@ -0,0 +1,11 @@
<html>
<body>
Reports problems related to workspace entity inheritance.
<p>
Verifies that an entity inherits from only one and only @Abstract workspace entity.
</p>
<p>
Verifies that an entity does not inherit fromWorkspaceEntity and EntitySource at the same time.
</p>
</body>
</html>
@@ -23,25 +23,16 @@
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.WorkspaceImplObsoleteInspection"
key="inspection.workspace.obsolete.model.display.name"/>
<localInspection language="kotlin"
shortName="WorkspaceImplAbsent"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.WorkspaceImplGenerationInspection"
key="inspection.workspace.impl.generation.display.name"/>
<localInspection language="kotlin"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.WorkspaceEntityMutableFieldInspection"
implementationClass="com.intellij.devkit.workspaceModel.inspections.WorkspaceEntityMutableFieldInspection"
key="inspection.workspace.mutable.field.display.name"/>
<localInspection language="kotlin"
projectType="INTELLIJ_PLUGIN"
groupBundle="messages.DevKitWorkspaceModelBundle"
groupPathKey="inspections.group.path" groupKey="inspections.group.workspace.model"
runForWholeFile="true"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.devkit.workspaceModel.inspections.WorkspaceInheritanceInspection"
key="inspection.workspace.inheritance.display.name"/>
<notificationGroup id="Incompatible codegen api versions"
displayType="BALLOON"
key="notification.workspace.incompatible.codegen.api.versions"/>
@@ -2,13 +2,19 @@ action.WorkspaceModelGeneration.text=Generate Workspace Model Implementation
inspections.group.path=Plugin DevKit
inspections.group.workspace.model=Workspace model
inspection.workspace.obsolete.model.display.name=Obsolete version of entity implementation
inspection.workspace.impl.generation.display.name=Generate implementation
inspection.workspace.msg.obsolete.implementation=Obsolete entity implementation
inspection.workspace.msg.regenerate.implementation=Regenerate implementation
inspection.workspace.impl.generation.display.name=Generate implementation
inspection.workspace.msg.generate.implementation=Generate implementation
inspection.workspace.msg.collect.class.metadata=Collect class metadata
inspection.workspace.msg.absent.implementation=Absent entity implementation
inspection.workspace.mutable.field.display.name=Unsupported 'var' field in entity
inspection.workspace.msg.absent.source.metadata=Absent EntitySource metadata
inspection.workspace.msg.absent.parent.source.metadata=Absent parent EntitySource metadata: {0}
inspection.workspace.msg.entity.and.source.inheritance=Cannot inherit EntitySource and WorkspaceEntity at the same time
inspection.workspace.msg.non.abstract.inheritance=Entities can only inherit '@Abstract' entities
inspection.workspace.msg.multiple.inheritance=Multiple inheritance is not supported in workspace entities
inspection.workspace.msg.user.implementation=Entity implementation has to be generated with the dedicated action
inspection.workspace.mutable.field.display.name=Unsupported 'var' field in an entity
inspection.workspace.inheritance.display.name=Workspace inheritance
inspection.workspace.msg.change.field.to.val=Change to 'val'
progress.title.generating.code=Generating code
@@ -25,3 +31,7 @@ notification.workspace.code.generation.not.available=Workspace code generation n
notification.workspace.code.generation.not.available.message=Entity code generation is not supported for plugins yet
action.WorkspaceModelGenerateAllModulesAction.text=Generate Workspace Model Implementation for All Modules
action.WorkspaceEntitiesJsonClipboard.text=Dump Workspace Entities to Clipboard in JSON Format
action.WorkspaceEntitiesJsonLog.text=Dump Workspace Entities to Log in JSON Format
action.WorkspaceEntitiesJsonLogFile.text=Dump Workspace Entities to Log File in JSON Format
progress.title.dumping.workspace.entities.json.to.clipboard=Dumping workspace entities JSON to clipboard
notification.title.cannot.find.log.directory=Cannot find the log directory
@@ -1,72 +0,0 @@
// 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.devkit.workspaceModel
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.workspaceModel.codegen.engine.SKIPPED_TYPES
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtVisitorVoid
internal class WorkspaceImplGenerationInspection: LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
override fun visitClass(klass: KtClass) {
val superTypeFqn = klass.getWorkspaceModelSuperType()
if (superTypeFqn == null) {
return
}
val highlightType: ProblemHighlightType
val descriptionTemplate: String
if (superTypeFqn == WorkspaceEntity::class.qualifiedName) { //is WorkspaceEntity implementation
if (!klass.isInterface()) return
if (klass.name in SKIPPED_TYPES) return
if (klass.isAbstractEntity()) return
if (klass.name == "Builder") return
val foundImplClasses = KotlinClassShortNameIndex["${klass.name}Impl", klass.project, GlobalSearchScope.allScope(klass.project)]
if (foundImplClasses.isEmpty()) {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.absent.implementation")
highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
} else {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.regenerate.implementation")
highlightType = ProblemHighlightType.INFORMATION
}
} else {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.collect.class.metadata")
highlightType = ProblemHighlightType.INFORMATION
}
holder.registerProblem(
klass.nameIdentifier!!, descriptionTemplate, highlightType,
GenerateWorkspaceModelFix(klass.nameIdentifier!!)
)
}
}
}
private class GenerateWorkspaceModelFix(psiElement: PsiElement) : LocalQuickFixOnPsiElement(psiElement) {
override fun getText() = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.generate.implementation")
override fun getFamilyName() = name
override fun invoke(project: Project, psiFile: PsiFile, startElement: PsiElement, endElement: PsiElement) {
if (!isIntellijProjectOrRegistryKeyIsSet(project)) {
generationNotAvailableNotification(project)
return
}
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val module = projectFileIndex.getModuleForFile(psiFile.virtualFile)
WorkspaceModelGenerator.getInstance(project).generate(module!!)
}
override fun startInWriteAction(): Boolean = false
}
@@ -1,64 +0,0 @@
// 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.devkit.workspaceModel
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.platform.workspace.storage.CodeGeneratorVersions
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtVisitorVoid
private val LOG = logger<WorkspaceImplObsoleteInspection>()
internal class WorkspaceImplObsoleteInspection: LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
override fun visitClass(klass: KtClass) {
if (!klass.isWorkspaceEntity()) return
val targetApiVersion = calculateTargetApiVersion(klass.resolveScope, klass.project)
if (targetApiVersion == null) {
LOG.info("Can't evaluate target API version for ${klass.name}")
return
}
if (klass.name == "Builder") return
val foundImplClasses = KotlinClassShortNameIndex.get("${klass.name}Impl", klass.project, GlobalSearchScope.allScope(klass.project))
if (foundImplClasses.isEmpty()) return
val implClass = foundImplClasses.first()
val apiVersion = (implClass as? KtClass)?.getApiVersion()
if (apiVersion == targetApiVersion) return
holder.registerProblem(klass.nameIdentifier!!, DevKitWorkspaceModelBundle.message("inspection.workspace.msg.obsolete.implementation"),
RegenerateWorkspaceModelFix(klass.nameIdentifier!!))
}
}
private fun calculateTargetApiVersion(scope: GlobalSearchScope, project: Project): Int? {
val generatorVersionsClass = JavaPsiFacade.getInstance(project).findClass(CodeGeneratorVersions::class.java.name, scope) ?: return null
val versionField = generatorVersionsClass.findFieldByName("API_VERSION_INTERNAL", false) ?: return null
return (versionField.initializer as? PsiLiteralExpression)?.value as? Int
}
}
private class RegenerateWorkspaceModelFix(psiElement: PsiElement) : LocalQuickFixOnPsiElement(psiElement) {
override fun getText() = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.regenerate.implementation")
override fun getFamilyName() = name
override fun invoke(project: Project, psiFile: PsiFile, startElement: PsiElement, endElement: PsiElement) {
if (!isIntellijProjectOrRegistryKeyIsSet(project)) {
generationNotAvailableNotification(project)
return
}
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val module = projectFileIndex.getModuleForFile(psiFile.virtualFile)
WorkspaceModelGenerator.getInstance(project).generate(module!!)
}
}
@@ -1,89 +0,0 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel
import com.intellij.platform.workspace.storage.*
import com.intellij.platform.workspace.storage.annotations.Abstract
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.parsing.parseNumericLiteral
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtSuperTypeListEntry
import org.jetbrains.kotlin.psi.KtUserType
import java.util.*
private val workspaceModelClasses: List<String> = listOfNotNull(
WorkspaceEntity::class.qualifiedName,
EntitySource::class.qualifiedName,
SymbolicEntityId::class.qualifiedName
)
/**
* Finds which of the [workspaceModelClasses] inherits this KtClass.
*
* @return parent class fully qualified name if inherits or null
*/
internal fun KtClass.getWorkspaceModelSuperType(): String? {
val superTypeList = LinkedList<KtSuperTypeListEntry>()
superTypeList.addAll(superTypeListEntries)
while (!superTypeList.isEmpty()) {
val superType = superTypeList.pop()
val resolvedKtClass = (superType.typeReference?.typeElement as? KtUserType)?.referenceExpression?.mainReference?.resolve() as? KtClass
?: continue
val resolvedKtClassFqn = resolvedKtClass.fqName?.asString()
if (workspaceModelClasses.contains(resolvedKtClassFqn)) return resolvedKtClassFqn
resolvedKtClass.superTypeListEntries.forEach { superTypeList.push(it) }
}
return null
}
internal fun KtClass.isWorkspaceEntity(): Boolean {
if (!isInterface()) return false
val superTypeFqn = getWorkspaceModelSuperType() ?: return false
return superTypeFqn == WorkspaceEntity::class.qualifiedName
}
internal fun KtClass.isAbstractEntity(): Boolean {
val annotationName = Abstract::class.simpleName
return annotationEntries.any { it.shortName?.identifier == annotationName }
}
internal fun KtClass.getApiVersion(): Int? {
val annotationName = GeneratedCodeApiVersion::class.simpleName
val annotation = annotationEntries.find { it.shortName?.identifier == annotationName }
if (annotation == null) {
error("$name should contain $annotationName")
}
if (annotation.valueArguments.size != 1) {
error("Annotation $annotationName at $name should contain only one argument")
}
val argumentExpression = annotation.valueArguments[0].getArgumentExpression() as? KtConstantExpression
if (argumentExpression == null) {
error("Annotation parameter should be int constant")
}
val elementType = argumentExpression.node.elementType
if (elementType == KtNodeTypes.INTEGER_CONSTANT) {
return parseNumericLiteral(argumentExpression.text, elementType)?.toInt()
}
return null
}
internal fun KtClass.getImplVersion(): Int? {
val annotationName = GeneratedCodeImplVersion::class.simpleName
val annotation = annotationEntries.find { it.shortName?.identifier == annotationName }
if (annotation == null) {
error("$name should contain $annotationName")
}
if (annotation.valueArguments.size != 1) {
error("Annotation $annotationName at $name should contain only one argument")
}
val argumentExpression = annotation.valueArguments[0].getArgumentExpression() as? KtConstantExpression
if (argumentExpression == null) {
error("Annotation parameter should be int constant")
}
val elementType = argumentExpression.node.elementType
if (elementType == KtNodeTypes.INTEGER_CONSTANT) {
return parseNumericLiteral(argumentExpression.text, elementType)?.toInt()
}
return null
}
@@ -0,0 +1,126 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.devkit.workspaceModel.DevKitWorkspaceModelBundle
import com.intellij.devkit.workspaceModel.WorkspaceModelGenerator
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.platform.workspace.storage.metadata.impl.MetadataStorageBase
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.workspaceModel.codegen.engine.SKIPPED_TYPES
import org.jetbrains.kotlin.idea.base.psi.KotlinPsiHeuristics
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtVisitorVoid
abstract class WorkspaceCodeAbsentInspectionBase : WorkspaceInspectionBase() {
protected abstract fun belongToSameModule(ktClass: KtClassOrObject, otherKtClass: KtClassOrObject): Boolean
private fun entitySourceIsPresentInMetadata(ktClass: KtClassOrObject): Boolean {
val jvmName = KotlinPsiHeuristics.getJvmName(ktClass) ?: return true
val searchScope = getModuleSearchScope(ktClass)
val moduleMetadata = KotlinClassShortNameIndex["MetadataStorageImpl", ktClass.project, searchScope]
val metadataFile: PsiFile = moduleMetadata.firstOrNull { metadataObject ->
belongToSameModule(metadataObject, ktClass) && metadataObject.getMatchingSuperTypes { superType ->
superType.fqName?.asString() == MetadataStorageBase::class.qualifiedName
}.any()
}?.containingFile ?: return false
// String.escapeDollar codegen/impl/metadata/StringExtensions.kt
val workspaceMetaName = jvmName.replace("$", "\\$")
// TODO: improve
val jvmNameOccurrence = metadataFile.getFileDocument().getImmutableCharSequence().indexOf(workspaceMetaName)
return jvmNameOccurrence != -1
}
private fun findSuperSourceAbsentInMetadata(ktClass: KtClassOrObject, visited: MutableSet<KtClass>): String? {
for (superType in ktClass.superTypeListEntries) {
val resolvedSuper = superType.typeReference?.resolveToKtClass() ?: continue
if (!visited.add(resolvedSuper)) continue
if (resolvedSuper.isWorkspaceEntitySource()) {
if (!entitySourceIsPresentInMetadata(resolvedSuper)) return resolvedSuper.name
val absentSuper = findSuperSourceAbsentInMetadata(resolvedSuper, visited)
if (absentSuper != null) return absentSuper
}
}
return null
}
private fun processWorkspaceEntityDeclaration(klass: KtClass, holder: ProblemsHolder) {
val psiElementForHighlighting = klass.getPsiElementForHighlighting() ?: return
val highlightType: ProblemHighlightType
val descriptionTemplate: String
if (klass.name in SKIPPED_TYPES) return
if (klass.isAbstractEntity()) return
val foundImpl = findWorkspaceEntityImplementation(klass, getModuleSearchScope(klass))
if (foundImpl == null) {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.absent.implementation")
highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
}
else {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.regenerate.implementation")
highlightType = ProblemHighlightType.INFORMATION
}
holder.registerProblem(
psiElementForHighlighting, descriptionTemplate, highlightType,
GenerateWorkspaceModelFix(psiElementForHighlighting)
)
}
private fun processEntitySource(klass: KtClassOrObject, holder: ProblemsHolder) {
val psiElementForHighlighting = klass.getPsiElementForHighlighting() ?: return
val highlightType: ProblemHighlightType
val descriptionTemplate: String
if (!entitySourceIsPresentInMetadata(klass)) {
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.absent.source.metadata")
highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
}
else {
val absentSuper = findSuperSourceAbsentInMetadata(klass, mutableSetOf()) ?: return
descriptionTemplate = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.absent.parent.source.metadata", absentSuper)
highlightType = ProblemHighlightType.GENERIC_ERROR_OR_WARNING
}
holder.registerProblem(
psiElementForHighlighting, descriptionTemplate, highlightType,
GenerateWorkspaceModelFix(psiElementForHighlighting)
)
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid = object : KtVisitorVoid() {
override fun visitClassOrObject(klass: KtClassOrObject) {
val superTypesFqns = klass.getWorkspaceSupers()
if (superTypesFqns.isEmpty()) {
return
}
if (klass is KtClass && klass.isWorkspaceEntityDeclaration()) {
processWorkspaceEntityDeclaration(klass, holder)
}
else if (klass.isWorkspaceEntitySource()) {
processEntitySource(klass, holder)
}
}
}
private class GenerateWorkspaceModelFix(psiElement: PsiElement) : LocalQuickFixOnPsiElement(psiElement) {
override fun getText() = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.generate.implementation")
override fun getFamilyName() = name
override fun invoke(project: Project, psiFile: PsiFile, startElement: PsiElement, endElement: PsiElement) {
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val module = projectFileIndex.getModuleForFile(psiFile.virtualFile)
WorkspaceModelGenerator.getInstance(project).generate(module!!)
}
override fun startInWriteAction(): Boolean = false
}
}
@@ -1,7 +1,8 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.codeInspection.*
import com.intellij.devkit.workspaceModel.DevKitWorkspaceModelBundle
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.IntellijInternalApi
import org.jetbrains.annotations.ApiStatus
@@ -15,10 +16,9 @@ import org.jetbrains.kotlin.psi.KtVisitorVoid
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceEntityMutableFieldInspection: LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = object : KtVisitorVoid() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid = object : KtVisitorVoid() {
override fun visitClass(klass: KtClass) {
if (!klass.isWorkspaceEntity()) return
if (klass.name == "Builder") return
if (!klass.isWorkspaceEntityDeclaration()) return
klass.getProperties().forEach { property ->
if (property.isVar) {
holder.registerProblem(property, DevKitWorkspaceModelBundle.message("inspection.workspace.mutable.field.display.name"),
@@ -0,0 +1,91 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.devkit.workspaceModel.DevKitWorkspaceModelBundle
import com.intellij.devkit.workspaceModel.WorkspaceModelGenerator
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.platform.workspace.storage.CodeGeneratorVersions
import com.intellij.platform.workspace.storage.GeneratedCodeApiVersion
import com.intellij.platform.workspace.storage.GeneratedCodeImplVersion
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiLiteralExpression
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.parsing.parseNumericLiteral
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtConstantExpression
import org.jetbrains.kotlin.psi.KtVisitorVoid
private val LOG = logger<WorkspaceImplObsoleteInspectionBase>()
abstract class WorkspaceImplObsoleteInspectionBase : WorkspaceInspectionBase() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid = object : KtVisitorVoid() {
override fun visitClass(klass: KtClass) {
val psiElementForHighlighting = klass.getPsiElementForHighlighting() ?: return
if (!klass.isWorkspaceEntityDeclaration()) return
val foundImplClass = findWorkspaceEntityImplementation(klass, getModuleSearchScope(klass))
if (foundImplClass == null) return
val targetApiVersion = calculateTargetApiVersion(klass.resolveScope, klass.project)
if (targetApiVersion == null) {
LOG.info("Can't evaluate target API version for ${klass.name}")
return
}
val implApiVersion = foundImplClass.getApiVersion()
if (implApiVersion == targetApiVersion) return
holder.registerProblem(psiElementForHighlighting, DevKitWorkspaceModelBundle.message("inspection.workspace.msg.obsolete.implementation"),
RegenerateWorkspaceModelFix(psiElementForHighlighting))
}
}
private fun calculateTargetApiVersion(scope: GlobalSearchScope, project: Project): Int? {
val generatorVersionsClass = JavaPsiFacade.getInstance(project).findClass(CodeGeneratorVersions::class.java.name, scope) ?: return null
val versionField = generatorVersionsClass.findFieldByName("API_VERSION_INTERNAL", false) ?: return null
return (versionField.initializer as? PsiLiteralExpression)?.value as? Int
}
private class RegenerateWorkspaceModelFix(psiElement: PsiElement) : LocalQuickFixOnPsiElement(psiElement) {
override fun getText() = DevKitWorkspaceModelBundle.message("inspection.workspace.msg.regenerate.implementation")
override fun getFamilyName() = name
override fun invoke(project: Project, psiFile: PsiFile, startElement: PsiElement, endElement: PsiElement) {
val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex
val module = projectFileIndex.getModuleForFile(psiFile.virtualFile)
WorkspaceModelGenerator.getInstance(project).generate(module!!)
}
}
}
private fun KtClass.getGeneratedAnnotationVersion(annotationFqName: String): Int? {
val annotation = findAnnotation(annotationFqName) ?: return null
if (annotation.valueArguments.size != 1) {
LOG.warn("Annotation $annotationFqName at $name should contain exactly one argument")
return null
}
val argumentExpression = annotation.valueArguments[0].getArgumentExpression() as? KtConstantExpression
val elementType = argumentExpression?.node?.elementType
if (elementType != KtNodeTypes.INTEGER_CONSTANT) {
LOG.warn("Annotation parameter of $annotationFqName at $name should be an int constant")
return null
}
return parseNumericLiteral(argumentExpression.text, elementType)?.toInt()
}
private fun KtClass.getApiVersion(): Int? {
val annotationFqName = GeneratedCodeApiVersion::class.qualifiedName!!
return getGeneratedAnnotationVersion(annotationFqName)
}
private fun KtClass.getImplVersion(): Int? {
val annotationFqName = GeneratedCodeImplVersion::class.simpleName!!
return getGeneratedAnnotationVersion(annotationFqName)
}
@@ -0,0 +1,61 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.devkit.workspaceModel.DevKitWorkspaceModelBundle
import com.intellij.openapi.util.IntellijInternalApi
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.VisibleForTesting
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtVisitorVoid
@VisibleForTesting
@IntellijInternalApi
@ApiStatus.Internal
class WorkspaceInheritanceInspection : LocalInspectionTool() {
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): KtVisitorVoid = object : KtVisitorVoid() {
override fun visitClassOrObject(klass: KtClassOrObject) {
val psiForHighlighting = klass.getPsiElementForHighlighting() ?: return
if (klass !is KtClass) return
if (!klass.isWorkspaceEntity()) return
if (klass.isWorkspaceEntityImplementation()) return
if (klass.isWorkspaceEntitySource()) {
holder.registerProblem(psiForHighlighting,
DevKitWorkspaceModelBundle.message("inspection.workspace.msg.entity.and.source.inheritance"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return
}
if (!klass.isInterface()) {
holder.registerProblem(psiForHighlighting,
DevKitWorkspaceModelBundle.message("inspection.workspace.msg.user.implementation"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return
}
val visited = mutableSetOf<KtClass>()
val superEntities = mutableSetOf<KtClass>()
for (superType in klass.superTypeListEntries) {
val resolvedSuper = superType.typeReference?.resolveToKtClass() ?: continue
if (!visited.add(resolvedSuper)) continue
if (!resolvedSuper.isWorkspaceEntity()) continue
if (resolvedSuper.isAbstractEntity()) {
superEntities.add(resolvedSuper)
continue
}
holder.registerProblem(psiForHighlighting,
DevKitWorkspaceModelBundle.message("inspection.workspace.msg.non.abstract.inheritance"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
return
}
if (superEntities.size > 1) {
holder.registerProblem(psiForHighlighting,
DevKitWorkspaceModelBundle.message("inspection.workspace.msg.multiple.inheritance"),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING)
}
}
}
}
@@ -0,0 +1,10 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.psi.KtClassOrObject
abstract class WorkspaceInspectionBase : LocalInspectionTool() {
protected abstract fun getModuleSearchScope(ktClass: KtClassOrObject): GlobalSearchScope
}
@@ -0,0 +1,111 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.SymbolicEntityId
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.annotations.Abstract
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import java.util.*
import java.util.function.Predicate
private val workspaceModelClasses: List<String> = listOfNotNull(
WorkspaceEntity::class.qualifiedName,
EntitySource::class.qualifiedName,
SymbolicEntityId::class.qualifiedName,
WorkspaceEntity.Builder::class.qualifiedName,
WorkspaceEntityBase::class.qualifiedName
)
/**
* Finds which [workspaceModelClasses] this KtClassOrObject inherits and caches the result.
*
* @return set of parent classes' fully qualified names
*/
internal fun KtClassOrObject.getWorkspaceSupers(): Set<String> {
return CachedValuesManager.getCachedValue(this) {
val workspaceSupersSequence = getMatchingSuperTypes { ktClass ->
workspaceModelClasses.contains(ktClass.fqName?.asString())
}
val workspaceSupers = workspaceSupersSequence.mapNotNull { it.fqName?.asString() }.toSet()
CachedValueProvider.Result(workspaceSupers, PsiModificationTracker.MODIFICATION_COUNT)
}
}
internal fun KtTypeReference.resolveToKtClass(): KtClass? {
val resolvedReference = (typeElement as? KtUserType)?.referenceExpression?.mainReference?.resolve()
return when (resolvedReference) {
is KtClass -> resolvedReference
is KtConstructor<*> -> resolvedReference.containingClass()
else -> null
}
}
internal fun KtClassOrObject.getMatchingSuperTypes(predicate: Predicate<KtClass>): Sequence<KtClass> = sequence {
val visited = mutableSetOf<KtClass>()
val superTypeList = LinkedList<KtSuperTypeListEntry>()
superTypeList.addAll(superTypeListEntries)
while (!superTypeList.isEmpty()) {
val superType = superTypeList.pop()
val resolvedKtClass = superType.typeReference?.resolveToKtClass() ?: continue
if (!visited.add(resolvedKtClass)) continue
if (predicate.test(resolvedKtClass)) yield(resolvedKtClass)
resolvedKtClass.superTypeListEntries.forEach { superTypeList.push(it) }
}
}
internal fun findWorkspaceEntityImplementation(entityClass: KtClass, searchScope: GlobalSearchScope): KtClass? {
val thisFqName = entityClass.fqName?.asString() ?: return null
val foundImplClasses = KotlinClassShortNameIndex["${entityClass.name}Impl", entityClass.project, searchScope]
val foundEntityImpls = foundImplClasses.filter { it is KtClass && it.isWorkspaceEntityImplementation() }
val foundImpl = foundEntityImpls.find { someImpl -> someImpl.getMatchingSuperTypes { it.fqName?.asString() == thisFqName }.any() }
return foundImpl as? KtClass
}
internal fun KtClass.isWorkspaceEntity(): Boolean {
return getWorkspaceSupers().contains(WorkspaceEntity::class.qualifiedName)
}
/**
* Check that a class is an **interface** that extends WorkspaceEntity, but not WorkspaceEntity.Builder, meaning that it is a declaration of
* an entity.
*/
internal fun KtClass.isWorkspaceEntityDeclaration(): Boolean {
if (!isInterface()) return false
val workspaceSupers = getWorkspaceSupers()
return workspaceSupers.contains(WorkspaceEntity::class.qualifiedName) &&
!workspaceSupers.contains(WorkspaceEntity.Builder::class.qualifiedName)
}
internal fun KtClass.isWorkspaceEntityImplementation(): Boolean {
val workspaceSupers = getWorkspaceSupers()
return workspaceSupers.contains(WorkspaceEntity::class.qualifiedName) && workspaceSupers.contains(WorkspaceEntityBase::class.qualifiedName)
}
internal fun KtClassOrObject.isWorkspaceEntitySource(): Boolean {
return getWorkspaceSupers().contains(EntitySource::class.qualifiedName)
}
internal fun KtClass.findAnnotation(annotationFqName: String): KtAnnotationEntry? {
for (annotationEntry in annotationEntries) {
val annotationClass = annotationEntry.typeReference?.resolveToKtClass() ?: continue
if (annotationClass.fqName?.asString() == annotationFqName) return annotationEntry
}
return null
}
internal fun KtClass.isAbstractEntity(): Boolean {
val annotationFqName = Abstract::class.qualifiedName!!
return findAnnotation(annotationFqName) != null
}
internal fun KtClassOrObject.getPsiElementForHighlighting(): PsiElement? = nameIdentifier ?: firstChild
@@ -0,0 +1,29 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
interface <warning descr="Absent entity implementation">EntityWithoutImplementation</warning> : WorkspaceEntity {
val property: String
val isValid: Boolean
}
interface <warning descr="Absent entity implementation">EntityWithFakeImplementation</warning> : WorkspaceEntity {
val property: String
val isValid: Boolean
}
internal class EntityWithFakeImplementationImpl() : EntityWithFakeImplementation {
override val property: String = ""
override val isValid: Boolean = false
}
interface EntityWithImplementation : WorkspaceEntity {
val property: String
val isValid: Boolean
}
internal class EntityWithImplementationImpl() : EntityWithImplementation, WorkspaceEntityBase() {
override val property: String = ""
override val isValid: Boolean = false
}
@@ -0,0 +1,35 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.EntitySource
// Fake MetadataStorageBase
abstract class MetadataStorageBase() {
@Suppress("UNUSED_PARAMETER")
protected fun addMetadataHash(typeFqn: String, metadataHash: Int) {
}
protected abstract fun initializeMetadataHash()
}
internal object MetadataStorageImpl : MetadataStorageBase() {
override fun initializeMetadataHash() {
addMetadataHash(typeFqn = "com.intellij.platform.workspace.storage.EntitySource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentObjectSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentClassSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentInterfaceSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentInheritedPresentSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.OuterClass1\$PresentInnerEntitySource", metadataHash = 0)
}
}
object <warning descr="Absent EntitySource metadata">PresentObjectSource</warning> : EntitySource
class <warning descr="Absent EntitySource metadata">PresentClassSource</warning> : EntitySource
interface <warning descr="Absent EntitySource metadata">PresentInterfaceSource</warning> : EntitySource
class <warning descr="Absent EntitySource metadata">PresentInheritedPresentSource</warning> : PresentInterfaceSource
class OuterClass1 {
class <warning descr="Absent EntitySource metadata">PresentInnerEntitySource</warning> : EntitySource
}
@@ -0,0 +1,49 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.metadata.impl.MetadataStorageBase
// Present in metadata
internal object MetadataStorageImpl: MetadataStorageBase() {
override fun initializeMetadataHash() {
addMetadataHash(typeFqn = "com.intellij.platform.workspace.storage.EntitySource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentObjectSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentClassSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentInterfaceSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentInheritedPresentSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.OuterClass1\$PresentInnerEntitySource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentInheritedAbsentSource", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentRecursive1", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentRecursive2", metadataHash = 0)
addMetadataHash(typeFqn = "com.intellij.workspaceModel.test.api.PresentRecursiveParentAbsent", metadataHash = 0)
}
}
object PresentObjectSource : EntitySource
class PresentClassSource : EntitySource
interface PresentInterfaceSource : EntitySource
class PresentInheritedPresentSource : PresentInterfaceSource
class OuterClass1 {
class PresentInnerEntitySource : EntitySource
}
// Absent in metadata
object <warning descr="Absent EntitySource metadata">AbsentObjectSource</warning> : EntitySource
class <warning descr="Absent EntitySource metadata">AbsentClassSource</warning> : EntitySource
interface <warning descr="Absent EntitySource metadata">AbsentInterfaceSource</warning> : EntitySource
class <warning descr="Absent EntitySource metadata">AbsentInheritedAbsentSource</warning> : AbsentInterfaceSource
class <warning descr="Absent parent EntitySource metadata: AbsentInterfaceSource">PresentInheritedAbsentSource</warning> : AbsentInterfaceSource
class OuterClass2 {
class <warning descr="Absent EntitySource metadata">AbsentInnerEntitySource</warning> : EntitySource
}
@@ -0,0 +1,54 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.EntitySource
import com.intellij.platform.workspace.storage.annotations.Abstract
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
interface EntityWithFakeImplementation : WorkspaceEntity {
val property: String
val isValid: Boolean
}
internal class <warning descr="Entity implementation has to be generated with the dedicated action">EntityWithFakeImplementationImpl</warning> : EntityWithFakeImplementation {
override val property: String = ""
override val isValid: Boolean = false
}
interface EntityWithImplementation : WorkspaceEntity {
val property: String
val isValid: Boolean
}
internal class EntityWithImplementationImpl() : EntityWithImplementation, WorkspaceEntityBase() {
override val property: String = ""
override val isValid: Boolean = false
}
interface OnlySource : EntitySource
object AnotherOnlySource : EntitySource
interface <warning descr="Cannot inherit EntitySource and WorkspaceEntity at the same time">EntityAndSource</warning> : WorkspaceEntity, EntitySource {
val property: String
}
interface SomeEntity : WorkspaceEntity
interface <warning descr="Entities can only inherit '@Abstract' entities">InheritsNonAbstract</warning> : SomeEntity
<warning descr="Entities can only inherit '@Abstract' entities">interface</warning><error descr="Name expected"> </error>: SomeEntity
@Abstract
interface AbstractEntity1 : WorkspaceEntity
interface NonAbstractEntity1 : AbstractEntity1
@Abstract
interface AbstractEntity2 : WorkspaceEntity
interface NonAbstractEntity2 : AbstractEntity2
interface <warning descr="Multiple inheritance is not supported in workspace entities">NonAbstractEntity3</warning> : AbstractEntity1, AbstractEntity2
interface <warning descr="Entities can only inherit '@Abstract' entities">NonAbstractEntity4</warning> : AbstractEntity1, NonAbstractEntity2
@@ -0,0 +1,16 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.WorkspaceEntity
@Target(AnnotationTarget.CLASS)
annotation class Abstract
@Abstract
interface NotWorkspaceAbstract : WorkspaceEntity
interface <warning descr="Entities can only inherit '@Abstract' entities">SomeEntity</warning> : NotWorkspaceAbstract
@com.intellij.platform.workspace.storage.annotations.Abstract
interface WorkspaceAbstract : WorkspaceEntity
interface AnotherEntity : WorkspaceAbstract
@@ -0,0 +1,29 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
interface WorkspaceEntity
interface NotWorkspaceEntity : WorkspaceEntity {
var property: String
}
interface AnotherNotWorkspaceEntity : WorkspaceEntity {
var flag: Boolean
}
interface EntitySource
object NotWorkspaceSource : EntitySource
class AnotherNotWorkspaceSource : EntitySource
interface NotWorkspaceMultipleInheritance : NotWorkspaceEntity, AnotherNotWorkspaceEntity, EntitySource
@Target(AnnotationTarget.CLASS)
annotation class GeneratedCodeApiVersion(val version: Int)
interface EntityWithOboleteImplementation : WorkspaceEntity
@GeneratedCodeApiVersion(2)
internal class EntityWithOboleteImplementationImpl() : EntityWithOboleteImplementation, WorkspaceEntityBase()
@@ -0,0 +1,33 @@
package com.intellij.workspaceModel.test.api
import com.intellij.platform.workspace.storage.GeneratedCodeApiVersion
import com.intellij.platform.workspace.storage.WorkspaceEntity
import com.intellij.platform.workspace.storage.annotations.Abstract
import com.intellij.platform.workspace.storage.impl.WorkspaceEntityBase
interface EntityWithFakeImplementation : WorkspaceEntity
internal class EntityWithFakeImplementationImpl() : EntityWithFakeImplementation
interface <warning descr="Obsolete entity implementation">EntityWithOboleteImplementation</warning> : WorkspaceEntity
@GeneratedCodeApiVersion(2)
internal class EntityWithOboleteImplementationImpl() : EntityWithOboleteImplementation, WorkspaceEntityBase()
interface EntityWithCorrectImplementation : WorkspaceEntity
@GeneratedCodeApiVersion(3)
internal class EntityWithCorrectImplementationImpl() : EntityWithCorrectImplementation, WorkspaceEntityBase()
@Abstract
interface AbstractEntity : WorkspaceEntity
interface <warning descr="Obsolete entity implementation">AnotherEntityWithOboleteImplementation</warning> : AbstractEntity
@GeneratedCodeApiVersion(2)
internal class AnotherEntityWithOboleteImplementationImpl() : AnotherEntityWithOboleteImplementation, WorkspaceEntityBase()
interface AnotherEntityWithCorrectImplementation : WorkspaceEntity
@GeneratedCodeApiVersion(3)
internal class AnotherEntityWithCorrectImplementationImpl() : AnotherEntityWithCorrectImplementation, WorkspaceEntityBase()
@@ -4,5 +4,5 @@ import com.intellij.platform.workspace.storage.WorkspaceEntity
interface MainEntity : WorkspaceEntity {
val property: String
<error descr="Unsupported 'var' field in entity">var <caret>isValid: Boolean</error>
<error descr="Unsupported 'var' field in an entity">var isValid: Boolean</error>
}
@@ -0,0 +1,24 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.openapi.util.IntellijInternalApi
abstract class WorkspaceCodeAbsentInspectionBaseTest : WorkspaceInspectionBaseTest() {
fun testEntityImplementation() {
doTest()
}
fun testEntitySourceMetadata() {
doTest()
}
fun testEntitySourceFakeMetadata() {
doTest()
}
fun testNotWorkspaceClasses() {
doTest()
}
}
@@ -1,35 +0,0 @@
// 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.devkit.workspaceModel.inspections
import com.intellij.openapi.application.PluginPathManager
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
abstract class WorkspaceEntityInspectionBase: LightJavaCodeInsightFixtureTestCase() {
private val TESTDATA_PATH = PluginPathManager.getPluginHomePathRelative("devkit") + "/intellij.devkit.workspaceModel/tests/testData/inspections/"
override fun setUp() {
super.setUp()
myFixture.createFile("Obj.kt", """
package com.intellij.platform.workspace.storage
interface Obj""".trimIndent())
myFixture.createFile("WorkspaceEntity.kt", """
package com.intellij.platform.workspace.storage
import com.intellij.platform.workspace.storage.Obj
interface WorkspaceEntity : Obj""".trimIndent())
}
override fun getBasePath() = TESTDATA_PATH
protected open fun doTest(fixName: String) {
val testName = getTestName(true)
val fileNameBefore = "$testName/entity.kt"
val fileNameAfter = "${testName}/entity_after.kt"
myFixture.testHighlighting(fileNameBefore)
val intention = myFixture.findSingleIntention(fixName)
myFixture.checkPreviewAndLaunchAction(intention)
myFixture.checkResultByFile(fileNameBefore, fileNameAfter, true)
}
}
@@ -3,16 +3,19 @@
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.devkit.workspaceModel.WorkspaceEntityMutableFieldInspection
import com.intellij.openapi.util.IntellijInternalApi
class WorkspaceEntityMutableFieldInspectionTest: WorkspaceEntityInspectionBase() {
abstract class WorkspaceEntityMutableFieldInspectionBaseTest : WorkspaceInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceEntityMutableFieldInspection())
}
fun testVarFieldForbidden() {
doTest("Change to 'val'")
doTestWithQuickFix("Change to 'val'")
}
fun testNotWorkspaceClasses() {
doTest()
}
}
@@ -0,0 +1,16 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.openapi.util.IntellijInternalApi
abstract class WorkspaceImplObsoleteInspectionBaseTest : WorkspaceInspectionBaseTest() {
fun testObsoleteImplementation() {
doTest()
}
fun testNotWorkspaceClasses() {
doTest()
}
}
@@ -0,0 +1,25 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@file:OptIn(IntellijInternalApi::class)
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.openapi.util.IntellijInternalApi
abstract class WorkspaceInheritanceInspectionBaseTest : WorkspaceInspectionBaseTest() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(WorkspaceInheritanceInspection())
}
fun testInheritance() {
doTest()
}
fun testNotWorkspaceAbstract() {
doTest()
}
fun testNotWorkspaceClasses() {
doTest()
}
}
@@ -0,0 +1,79 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.workspaceModel.inspections
import com.intellij.openapi.application.PluginPathManager
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import org.intellij.lang.annotations.Language
abstract class WorkspaceInspectionBaseTest : LightJavaCodeInsightFixtureTestCase() {
override fun getBasePath() = TESTDATA_PATH
private fun getBeforeAndAfterFileNames(): Pair<String, String> {
val testName = getTestName(true)
val fileNameBefore = "$testName/entity.kt"
val fileNameAfter = "${testName}/entity_after.kt"
return fileNameBefore to fileNameAfter
}
protected fun addKotlinFile(relativePath: String, @Language("kotlin") fileText: String) {
myFixture.addFileToProject(relativePath, fileText)
}
override fun setUp() {
super.setUp()
addKotlinFile("EntitySource.kt", """
package com.intellij.platform.workspace.storage
interface EntitySource
""".trimIndent())
addKotlinFile("Abstract.kt", """
package com.intellij.platform.workspace.storage.annotations
@Target(AnnotationTarget.CLASS)
annotation class Abstract
""".trimIndent())
addKotlinFile("generatedCodeCompatibility.kt", """
package com.intellij.platform.workspace.storage
object CodeGeneratorVersions {
private const val API_VERSION_INTERNAL = 3
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class GeneratedCodeApiVersion(val version: Int)
""".trimIndent())
addKotlinFile("WorkspaceEntity.kt", """
package com.intellij.platform.workspace.storage
import com.intellij.platform.workspace.storage.annotations.Abstract
@Abstract
interface WorkspaceEntity
""".trimIndent())
addKotlinFile("MetadataStorageBase.kt", """
package com.intellij.platform.workspace.storage.metadata.impl
abstract class MetadataStorageBase() {
protected fun addMetadataHash(typeFqn: String, metadataHash: Int) {}
protected abstract fun initializeMetadataHash()
}
""".trimIndent())
addKotlinFile("WorkspaceEntityBase.kt", """
package com.intellij.platform.workspace.storage.impl
import com.intellij.platform.workspace.storage.WorkspaceEntity
abstract class WorkspaceEntityBase() : WorkspaceEntity
""".trimIndent())
}
protected fun doTestWithQuickFix(fixName: String) {
val (fileNameBefore, fileNameAfter) = getBeforeAndAfterFileNames()
myFixture.testHighlighting(fileNameBefore)
val quickFix = myFixture.getAllQuickFixes().find { it.text == fixName }
assertNotNull("Fix $fixName not found", quickFix)
myFixture.checkPreviewAndLaunchAction(quickFix!!)
myFixture.checkResultByFile(fileNameBefore, fileNameAfter, true)
}
protected fun doTest() {
val (fileNameBefore, _) = getBeforeAndAfterFileNames()
myFixture.testHighlighting(fileNameBefore)
}
companion object {
private val TESTDATA_PATH = PluginPathManager.getPluginHomePathRelative("devkit") + "/intellij.devkit.workspaceModel/tests/testData/inspections/"
}
}