diff --git a/plugins/devkit/devkit-core/resources/inspectionDescriptions/WrongPluginModuleAlias.html b/plugins/devkit/devkit-core/resources/inspectionDescriptions/WrongPluginModuleAlias.html new file mode 100644 index 000000000000..bfb1bbf02994 --- /dev/null +++ b/plugins/devkit/devkit-core/resources/inspectionDescriptions/WrongPluginModuleAlias.html @@ -0,0 +1,11 @@ + + +

+ Reports restricted plugin module aliases in non-JetBrains plugin descriptors. +

+

+ Aliases starting with com.intellij.* are reserved for plugins developed by JetBrains and must not be declared + by third-party plugins. +

+ + diff --git a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml index 16012222ac3c..18741917a5ed 100644 --- a/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml +++ b/plugins/devkit/devkit-core/resources/intellij.devkit.core.xml @@ -551,6 +551,13 @@ implementationClass="org.jetbrains.idea.devkit.inspections.ContentModuleNamespaceInspection" key="inspection.content.module.namespace.name"/> + + = module.value + val alias = value.value ?: return + if (alias.startsWith(reservedPrefix)) { + holder.createProblem(value, message("inspection.plugin.module.alias.message")) + } + } + + private fun isDevelopedByJetBrains(element: PluginModule): Boolean { + val vendor = DomUtil.findDomElement(element.xmlElement, IdeaPlugin::class.java)?.vendor ?: return false + return PluginManagerCore.isDevelopedByJetBrains(vendor.stringValue) + } + +} diff --git a/plugins/devkit/devkit-java-tests/BUILD.bazel b/plugins/devkit/devkit-java-tests/BUILD.bazel index d495496675f8..2484f5d7a0c0 100644 --- a/plugins/devkit/devkit-java-tests/BUILD.bazel +++ b/plugins/devkit/devkit-java-tests/BUILD.bazel @@ -47,6 +47,7 @@ jvm_library( "//plugins/devkit/devkit-tests:testFramework_test_lib", "//platform/testFramework", "//platform/testFramework:testFramework_test_lib", + "//platform/testFramework/junit5/codeInsight", "//java/compiler:compiler-tests_test_lib", "//java/testFramework", "//xml/dom-tests:dom-tests_test_lib", diff --git a/plugins/devkit/devkit-java-tests/intellij.devkit.java.tests.iml b/plugins/devkit/devkit-java-tests/intellij.devkit.java.tests.iml index 6aa6683bbba8..e21aa938edad 100644 --- a/plugins/devkit/devkit-java-tests/intellij.devkit.java.tests.iml +++ b/plugins/devkit/devkit-java-tests/intellij.devkit.java.tests.iml @@ -33,6 +33,7 @@ + diff --git a/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/WrongPluginModuleAliasInspectionTest.kt b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/WrongPluginModuleAliasInspectionTest.kt new file mode 100644 index 000000000000..a6e33107eafb --- /dev/null +++ b/plugins/devkit/devkit-java-tests/testSrc/org/jetbrains/idea/devkit/inspections/WrongPluginModuleAliasInspectionTest.kt @@ -0,0 +1,104 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.idea.devkit.inspections + +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.project.IntelliJProjectUtil +import com.intellij.openapi.project.Project +import com.intellij.platform.testFramework.junit5.codeInsight.fixture.codeInsightFixture +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.fixture.TestFixture +import com.intellij.testFramework.junit5.fixture.moduleFixture +import com.intellij.testFramework.junit5.fixture.projectFixture +import com.intellij.testFramework.junit5.fixture.tempPathFixture +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.nio.file.Path +import java.util.stream.Stream +import kotlin.streams.asStream + +@TestApplication +class WrongPluginModuleAliasInspectionTest { + + private val tempDir: TestFixture = tempPathFixture() + private val project: TestFixture = projectFixture(tempDir, openAfterCreation = true) + + @Suppress("unused") // required by codeInsightFixture + private val module by project.moduleFixture(tempDir, addPathToSourceRoot = true) + private val fixture = codeInsightFixture(project, tempDir) + + @BeforeEach + fun setUp() { + fixture.get().enableInspections(WrongPluginModuleAliasInspection()) + } + + @DisplayName("Plugin module alias inspection test") + @ParameterizedTest(name = "{0} when vendor is ''{1}'', module prefix is ''{2}'', and IntelliJ Platform project is {3}") + @MethodSource("testCases") + fun testPluginModuleAlias( + testExpectationPrefix: String, + vendor: String, + modulePrefix: String, + isIntelliJProject: Boolean, + ) { + if (isIntelliJProject) { + IntelliJProjectUtil.markAsIntelliJPlatformProject(project.get(), true) + } + + val moduleValue = "${modulePrefix}myFeature" + val errorTag = if (testExpectationPrefix == ERROR_EXPECTED) { + """$moduleValue""" + } + else { + moduleValue + } + + doTest(""" + + $vendor + + + """.trimIndent()) + } + + private fun doTest(@Language("XML") xml: String) { + fixture.get().apply { + configureByText("plugin.xml", xml) + testHighlighting(false, false, true) + } + } + + companion object { + private const val ERROR_EXPECTED = "should report error" + private const val ERROR_NOT_EXPECTED = "should not report error" + + @JvmStatic + fun testCases(): Stream = + cartesianProduct( + listOf("JetBrains", "JetBrains s.r.o.", "JetBrains, Google", "Acme"), + listOf("com.intellij.", "com.example."), + listOf(true, false) + ).map { (vendor, prefix, isIntelliJProject) -> + val expectError = !PluginManagerCore.isDevelopedByJetBrains(vendor) && prefix == "com.intellij." && !isIntelliJProject + val testExpectationPrefix = if (expectError) ERROR_EXPECTED else ERROR_NOT_EXPECTED + Arguments.of(testExpectationPrefix, vendor, prefix, isIntelliJProject) + }.asStream() + + private fun cartesianProduct( + listA: List, + listB: List, + listC: List, + ): Sequence> = sequence { + for (a in listA) { + for (b in listB) { + for (c in listC) { + yield(Triple(a, b, c)) + } + } + } + } + } +}