[Kotlin] Added inspection to find expect declarations without corresponding actual implementation in a module

^KTIJ-28278 fixed


Merge-request: IJ-MR-134654
Merged-by: Frederik Haselmeier <Frederik.Haselmeier@jetbrains.com>

GitOrigin-RevId: 084dab27001d11ddecf7ceeac8f608fa8fdb6f6b
This commit is contained in:
Frederik Haselmeier
2024-05-17 20:32:58 +00:00
committed by intellij-monorepo-bot
parent 4ed061c1f3
commit a98f167e02
60 changed files with 347 additions and 1 deletions

View File

@@ -1964,6 +1964,7 @@ open.moved.method.in.editor=Open moved method in editor
to.fully.qualified.name=To (fully qualified name):
incomplete.destructuring.declaration.text=Incomplete destructuring declaration
incomplete.destructuring.fix.family.name=Add missing variables to destructuring declaration
no.actual.for.expect.declaration=No actual for expect declaration in module(s): {0}
#Structural Search
category.class=Kotlin/Class-based
@@ -2148,6 +2149,7 @@ inspection.redundant.async.display.name=Redundant 'async' call
inspection.main.function.return.unit.display.name=Main function should return 'Unit'
inspection.move.variable.declaration.into.when.display.name=Variable declaration could be moved inside 'when'
inspection.move.lambda.outside.parentheses.display.name=Lambda argument inside parentheses
inspection.no.actual.for.expect.display.name=No actual for expect declaration
inspection.can.sealed.subclass.be.object.display.name=Sealed subclass without state and overridden equals
inspection.public.api.implicit.type.display.name=Public API declaration with implicit return type
inspection.redundant.companion.reference.display.name=Redundant 'Companion' reference

View File

@@ -0,0 +1,6 @@
<html>
<body>
Reports <code>expect</code> declarations that do not have a corresponding
<code>actual</code> declaration in an implementing multi-platform module.
</body>
</html>

View File

@@ -280,6 +280,14 @@
enabledByDefault="true"
level="WEAK WARNING"
language="kotlin" key="inspection.move.lambda.outside.parentheses.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.NoActualForExpectInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.kotlin"
enabledByDefault="true"
level="ERROR"
language="kotlin" key="inspection.no.actual.for.expect.display.name" bundle="messages.KotlinBundle"/>
<localInspection implementationClass="org.jetbrains.kotlin.idea.k2.codeinsight.inspections.PackageDirectoryMismatchInspection"
groupPath="Kotlin"
groupBundle="messages.KotlinBundle" groupKey="group.names.java.interop.issues"

View File

@@ -0,0 +1,60 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.codeinsight.inspections
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.module.Module
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.findParentOfType
import org.jetbrains.kotlin.idea.base.facet.implementedModules
import org.jetbrains.kotlin.idea.base.facet.implementingModules
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
import org.jetbrains.kotlin.idea.searching.kmp.findAllActualForExpect
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtModifierList
import org.jetbrains.kotlin.psi.KtVisitorVoid
class NoActualForExpectInspection : AbstractKotlinInspection() {
private fun Module.getLeaves(): Set<Module> {
val implementingModules = implementingModules
if (implementingModules.isEmpty()) return setOf(this)
return implementingModules.flatMapTo(mutableSetOf()) { it.getLeaves() }
}
private fun Module.hasActualInParentOrSelf(allModulesWithActual: Set<Module>): Boolean {
return this in allModulesWithActual || implementedModules.any { it in allModulesWithActual }
}
override fun buildVisitor(
holder: ProblemsHolder,
isOnTheFly: Boolean,
session: LocalInspectionToolSession
): PsiElementVisitor {
return object : KtVisitorVoid() {
override fun visitModifierList(list: KtModifierList) {
val module = list.module ?: return
val expectModifier = list.getModifier(KtTokens.EXPECT_KEYWORD) ?: return
val parentDeclaration = list.findParentOfType<KtDeclaration>() ?: return
val leaves = module.getLeaves()
val foundActuals = parentDeclaration.findAllActualForExpect()
.mapNotNullTo(mutableSetOf()) { it.element?.module }.toSet()
val missingActuals = leaves.filter { module ->
!module.hasActualInParentOrSelf(foundActuals)
}
if (missingActuals.isEmpty()) return
val missingModulesWithActuals = missingActuals.joinToString { it.name.substringAfterLast('.') }
holder.registerProblem(
expectModifier,
KotlinBundle.message("no.actual.for.expect.declaration", missingModulesWithActuals)
)
}
}
}
}

View File

@@ -49,5 +49,6 @@
<orderEntry type="module" module-name="kotlin.code-insight.inspections.k2" scope="TEST" />
<orderEntry type="library" scope="TEST" name="kotlinc.kotlin-compiler-common" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" scope="TEST" />
<orderEntry type="module" module-name="kotlin.base.project-structure" scope="TEST" />
</component>
</module>

View File

@@ -0,0 +1,42 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.inspections.tests
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils
import org.jetbrains.kotlin.idea.inspections.runInspection
import org.jetbrains.kotlin.idea.k2.codeinsight.inspections.NoActualForExpectInspection
import java.io.File
abstract class AbstractK2ActualExpectTest : AbstractK2MultiplatformTest() {
override fun setUp() {
super.setUp()
}
override fun doTest(testPath: String) {
super.doTest(testPath)
val configFile = File(testPath, "missing_actuals.txt")
assertExists(configFile)
val fileText = configFile.readText()
val missingActualDirective = InTextDirectivesUtils.findLineWithPrefixRemoved(fileText, "// MISSING_ACTUALS:")
assertNotNull("Missing actuals declaration", missingActualDirective)
val expectedMissingActualModules = missingActualDirective!!.substringAfter(":").trim()
.takeIf { !it.isEmpty() }?.split(",")?.map { it.trim() } ?: emptyList()
val problemDescriptors = runInspection(
NoActualForExpectInspection::class.java,
project,
settings = null
).problemElements.values.map { it.descriptionTemplate }
assertTrue("Found more than one problemDescriptor", problemDescriptors.size <= 1)
val problemDescriptor = problemDescriptors.firstOrNull()
val missingActuals = if (problemDescriptor.isNullOrEmpty()) {
emptyList()
} else {
problemDescriptor.substringAfter(":").trim().split(",").map { it.trim() }.filter { !it.isBlank() }
}
assertSameElements(expectedMissingActualModules, missingActuals)
}
override fun tearDown() {
super.tearDown()
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.inspections.tests
import com.intellij.util.ThrowableRunnable
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
import org.jetbrains.kotlin.idea.base.projectStructure.compositeAnalysis.KotlinMultiplatformAnalysisModeComponent
import org.jetbrains.kotlin.idea.multiplatform.setupMppProjectFromDirStructure
import org.jetbrains.kotlin.idea.test.AbstractMultiModuleTest
import org.jetbrains.kotlin.idea.test.IDEA_TEST_DATA_DIR
import org.jetbrains.kotlin.idea.test.runAll
import java.io.File
abstract class AbstractK2MultiplatformTest : AbstractMultiModuleTest() {
override fun getTestDataDirectory() = IDEA_TEST_DATA_DIR.resolve("multiplatform")
override val pluginMode: KotlinPluginMode = KotlinPluginMode.K2
override fun setUp() {
super.setUp()
KotlinMultiplatformAnalysisModeComponent.setMode(project, KotlinMultiplatformAnalysisModeComponent.Mode.COMPOSITE)
}
protected open fun doTest(testPath: String) {
setupMppProjectFromDirStructure(File(testPath))
}
override fun tearDown() {
runAll(
ThrowableRunnable {
KotlinMultiplatformAnalysisModeComponent.setMode(project, KotlinMultiplatformAnalysisModeComponent.Mode.SEPARATE)
},
ThrowableRunnable { super.tearDown() }
)
}
}

View File

@@ -0,0 +1,80 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.kotlin.idea.k2.inspections.tests;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.jetbrains.kotlin.idea.base.test.TestRoot;
import org.junit.runner.RunWith;
/**
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
* DO NOT MODIFY MANUALLY.
*/
@SuppressWarnings("all")
@TestRoot("code-insight/inspections-k2/tests")
@TestDataPath("$CONTENT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/multiplatform/actualExpect")
public class K2ActualExpectTestGenerated extends AbstractK2ActualExpectTest {
private void runTest(String testDataFilePath) throws Exception {
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
}
@TestMetadata("actualizedClass")
public void testActualizedClass() throws Exception {
runTest("testData/multiplatform/actualExpect/actualizedClass/");
}
@TestMetadata("actualizedFunction")
public void testActualizedFunction() throws Exception {
runTest("testData/multiplatform/actualExpect/actualizedFunction/");
}
@TestMetadata("actualizedInParent")
public void testActualizedInParent() throws Exception {
runTest("testData/multiplatform/actualExpect/actualizedInParent/");
}
@TestMetadata("actualizedTypealias")
public void testActualizedTypealias() throws Exception {
runTest("testData/multiplatform/actualExpect/actualizedTypealias/");
}
@TestMetadata("missedActual")
public void testMissedActual() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActual/");
}
@TestMetadata("missedActualClass")
public void testMissedActualClass() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActualClass/");
}
@TestMetadata("missedActualFunction")
public void testMissedActualFunction() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActualFunction/");
}
@TestMetadata("missedActualInSubtree")
public void testMissedActualInSubtree() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActualInSubtree/");
}
@TestMetadata("missedActualObject")
public void testMissedActualObject() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActualObject/");
}
@TestMetadata("missedActualSingleTarget")
public void testMissedActualSingleTarget() throws Exception {
runTest("testData/multiplatform/actualExpect/missedActualSingleTarget/");
}
@TestMetadata("onlyListMissingLeaves")
public void testOnlyListMissingLeaves() throws Exception {
runTest("testData/multiplatform/actualExpect/onlyListMissingLeaves/");
}
}

View File

@@ -0,0 +1,4 @@
MODULE common { platform=[JVM] }
MODULE jvm { platform=[JVM] }
jvm -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,4 @@
MODULE common { platform=[JVM] }
MODULE jvm { platform=[JVM] }
jvm -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,8 @@
MODULE jvmCommon { platform=[JVM] }
MODULE jvmA { platform=[JVM] }
MODULE jvmB { platform=[JVM] }
MODULE common { platform=[JVM] }
jvmCommon -> common { kind=DEPENDS_ON }
jvmA -> jvmCommon { kind=DEPENDS_ON }
jvmB -> jvmCommon { kind=DEPENDS_ON }

View File

@@ -0,0 +1,4 @@
MODULE common { platform=[JVM] }
MODULE jvm { platform=[JVM] }
jvm -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,3 @@
class Other
actual typealias Test = Other

View File

@@ -0,0 +1,6 @@
MODULE a { platform=[JVM] }
MODULE b { platform=[JVM] }
MODULE common { platform=[JVM] }
a -> common { kind=DEPENDS_ON }
b -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,6 @@
MODULE a { platform=[JVM] }
MODULE b { platform=[JVM] }
MODULE common { platform=[JVM] }
a -> common { kind=DEPENDS_ON }
b -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,10 @@
MODULE jvmCommon { platform=[JVM] }
MODULE jvmA { platform=[JVM] }
MODULE jvmB { platform=[JVM] }
MODULE jvmC { platform=[JVM] }
MODULE common { platform=[JVM] }
jvmCommon -> common { kind=DEPENDS_ON }
jvmA -> jvmCommon { kind=DEPENDS_ON }
jvmB -> jvmCommon { kind=DEPENDS_ON }
jvmC -> common { kind=DEPENDS_ON }

View File

@@ -0,0 +1,8 @@
MODULE jvmCommon { platform=[JVM] }
MODULE jvmA { platform=[JVM] }
MODULE jvmB { platform=[JVM] }
MODULE common { platform=[JVM] }
jvmCommon -> common { kind=DEPENDS_ON }
jvmA -> jvmCommon { kind=DEPENDS_ON }
jvmB -> jvmCommon { kind=DEPENDS_ON }

View File

@@ -50,5 +50,7 @@
<orderEntry type="module" module-name="kotlin.refactorings.common" />
<orderEntry type="module" module-name="intellij.java.impl" />
<orderEntry type="module" module-name="kotlin.project-configuration" />
<orderEntry type="module" module-name="kotlin.base.facet" />
<orderEntry type="module" module-name="intellij.platform.core" />
</component>
</module>

View File

@@ -6,6 +6,7 @@ import org.jetbrains.kotlin.idea.k2.codeInsight.inspections.shared.AbstractShare
import org.jetbrains.kotlin.idea.k2.codeInsight.inspections.shared.AbstractSharedK2LocalInspectionTest
import org.jetbrains.kotlin.idea.k2.codeInsight.inspections.shared.AbstractSharedK2MultiFileQuickFixTest
import org.jetbrains.kotlin.idea.k2.codeInsight.inspections.shared.idea.kdoc.AbstractSharedK2KDocHighlightingTest
import org.jetbrains.kotlin.idea.k2.inspections.tests.AbstractK2ActualExpectTest
import org.jetbrains.kotlin.idea.k2.inspections.tests.AbstractK2InspectionTest
import org.jetbrains.kotlin.idea.k2.inspections.tests.AbstractK2LocalInspectionAndGeneralHighlightingTest
import org.jetbrains.kotlin.idea.k2.inspections.tests.AbstractK2LocalInspectionTest
@@ -15,6 +16,7 @@ import org.jetbrains.kotlin.idea.k2.quickfix.tests.AbstractK2MultiFileQuickFixTe
import org.jetbrains.kotlin.idea.k2.quickfix.tests.AbstractK2QuickFixTest
import org.jetbrains.kotlin.testGenerator.model.*
import org.jetbrains.kotlin.testGenerator.model.GroupCategory.*
import org.jetbrains.kotlin.testGenerator.model.Patterns.DIRECTORY
internal fun MutableTWorkspace.generateK2InspectionTests() {
@@ -98,6 +100,10 @@ internal fun MutableTWorkspace.generateK2InspectionTests() {
model("${idea}/multiFileLocalInspections/redundantQualifierName", pattern = pattern)
model("code-insight/inspections-k2/tests/testData/multiFileInspectionsLocal", pattern = pattern)
}
testClass<AbstractK2ActualExpectTest> {
model("code-insight/inspections-k2/tests/testData/multiplatform/actualExpect/", pattern = DIRECTORY, isRecursive = false)
}
}
testGroup("code-insight/inspections-k2/tests", category = QUICKFIXES, testDataPath = "../../..") {
testClass<AbstractK2QuickFixTest> {
@@ -131,7 +137,6 @@ internal fun MutableTWorkspace.generateK2InspectionTests() {
model("inspections", pattern = pattern)
model("inspectionsLocal", pattern = pattern)
}
}
testGroup("code-insight/inspections-shared/tests/k2", category = HIGHLIGHTING, testDataPath = "../testData") {