[project model] report an error if 'moduleConfigurationEditorProvider' extension requires 'Module' as constructor argument (IJPL-179175)

'moduleConfigurationEditorProvider' extension point was registered as a module-level. However, it seems that no implementations use this ability, and can be converted to application-level extensions automatically. To avoid breaking compatibility, the extension point was converted to 'bean class', and tries to call the constructor with 'Module' parameter if no-arg constructor isn't available, and report an error in such cases.

GitOrigin-RevId: 8390fda7aa09f09b6df8178d40b6269b26b0292f
This commit is contained in:
Nikolay Chashnikov
2025-06-16 23:57:44 +03:00
committed by intellij-monorepo-bot
parent a8cdcef3e5
commit b94efffb44
4 changed files with 91 additions and 4 deletions

View File

@@ -20,6 +20,7 @@ import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.roots.ui.configuration.impl.ModuleConfigurationEditorProviderEp;
import com.intellij.platform.workspace.storage.MutableEntityStorage;
import com.intellij.ui.navigation.History;
import com.intellij.ui.navigation.Place;
@@ -43,7 +44,7 @@ import java.util.List;
* @author Eugene Zhuravlev
*/
public abstract class ModuleEditor implements Place.Navigator, Disposable {
private static final ExtensionPointName<ModuleConfigurationEditorProvider> EP_NAME =
private static final ExtensionPointName<ModuleConfigurationEditorProviderEp> EP_NAME =
new ExtensionPointName<>("com.intellij.moduleConfigurationEditorProvider");
private static final Logger LOG = Logger.getInstance(ModuleEditor.class);
@@ -160,7 +161,8 @@ public abstract class ModuleEditor implements Place.Navigator, Disposable {
private void createEditors(@NotNull Module module) {
ModuleConfigurationState state = createModuleConfigurationState();
for (ModuleConfigurationEditorProvider provider : EP_NAME.getExtensionList(module)) {
for (ModuleConfigurationEditorProviderEp providerEp : EP_NAME.getExtensionList()) {
ModuleConfigurationEditorProvider provider = providerEp.getOrCreateInstance(module);
ModuleConfigurationEditor[] editors = provider.createEditors(state);
if (editors.length > 0 && provider instanceof ModuleConfigurationEditorProviderEx &&
((ModuleConfigurationEditorProviderEx)provider).isCompleteEditorSet()) {

View File

@@ -0,0 +1,80 @@
// 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.openapi.roots.ui.configuration.impl
import com.intellij.diagnostic.PluginException
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.fileLogger
import com.intellij.openapi.extensions.PluginAware
import com.intellij.openapi.extensions.PluginDescriptor
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleConfigurationEditor
import com.intellij.openapi.roots.ui.configuration.ModuleConfigurationEditorProvider
import com.intellij.util.xmlb.annotations.Attribute
import org.jetbrains.annotations.ApiStatus
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
/**
* Implements [ModuleConfigurationEditorProvider] extension point supporting the deprecated way when an extension is instantiated with
* [Module] as a constructor parameter.
* When this possibility is removed, the extension point can be converted to a regular 'interface' extension point.
*/
class ModuleConfigurationEditorProviderEp : PluginAware {
private var pluginDescriptor: PluginDescriptor? = null
@Attribute("implementation")
@JvmField
var implementationClass: String? = null
@Volatile
private var cachedInstance: ModuleConfigurationEditorProvider? = null
override fun setPluginDescriptor(pluginDescriptor: PluginDescriptor) {
this.pluginDescriptor = pluginDescriptor
}
@ApiStatus.Internal
fun getOrCreateInstance(module: Module): ModuleConfigurationEditorProvider {
try {
cachedInstance?.let {
return it
}
val implementationClass = implementationClass ?: error("'implementation' attribute is not specified'")
val pluginDescriptor = pluginDescriptor ?: error("plugin descriptor is not set")
val instanceClass = ApplicationManager.getApplication().loadClass<ModuleConfigurationEditor>(implementationClass, pluginDescriptor)
val lookup = MethodHandles.privateLookupIn(instanceClass, methodLookup)
val defaultConstructor: MethodHandle
try {
defaultConstructor = lookup.findConstructor(instanceClass, emptyConstructorMethodType)
}
catch (_: NoSuchMethodException) {
val constructorWithModule = lookup.findConstructor(instanceClass, moduleMethodType)
LOG.error(PluginException("""
|'moduleConfigurationEditorProvider' extensions must have a constructor without parameters and take Module instance from
|the parameter of 'createEditors' function ('state.getCurrentRootModel().getModule()'), but '$implementationClass'
|has a constructor taking 'Module' as parameter.
""".trimMargin(), pluginDescriptor.pluginId))
return constructorWithModule.invoke(module) as ModuleConfigurationEditorProvider
}
synchronized(this) {
cachedInstance?.let {
return it
}
val provider = defaultConstructor.invoke() as ModuleConfigurationEditorProvider
cachedInstance = provider
return provider
}
}
catch (t: Throwable) {
throw PluginException("Failed to instantiate ModuleConfigurationEditorProvider ($implementationClass)", t, pluginDescriptor?.pluginId)
}
}
}
private val LOG = fileLogger()
private val methodLookup = MethodHandles.lookup()
private val emptyConstructorMethodType: MethodType = MethodType.methodType(Void.TYPE)
private val moduleMethodType = MethodType.methodType(Void.TYPE, Module::class.java)

View File

@@ -11,6 +11,9 @@ import org.jetbrains.annotations.NotNull;
* &nbsp;&nbsp;&lt;moduleConfigurationEditorProvider implementation="qualified-class-name"/&gt;
* &lt;/extensions&gt;
* </pre>
*
* The implementation must have a no-arg constructor. The {@link com.intellij.openapi.module.Module} instance can be taken from
* {@code state} parameter of {@link #createEditors} ({@code state.getCurrentRootModel().getModule()}).
*/
public interface ModuleConfigurationEditorProvider {
@NotNull ModuleConfigurationEditor @NotNull [] createEditors(@NotNull ModuleConfigurationState state);

View File

@@ -543,8 +543,10 @@
<with attribute="implementationClass" implements="com.intellij.testIntegration.TestCreator"/>
</extensionPoint>
<extensionPoint name="moduleConfigurationEditorProvider" interface="com.intellij.openapi.roots.ui.configuration.ModuleConfigurationEditorProvider"
area="IDEA_MODULE" dynamic="true"/>
<extensionPoint name="moduleConfigurationEditorProvider" beanClass="com.intellij.openapi.roots.ui.configuration.impl.ModuleConfigurationEditorProviderEp"
dynamic="true">
<with attribute="implementation" implements="com.intellij.openapi.roots.ui.configuration.ModuleConfigurationEditorProvider"/>
</extensionPoint>
<extensionPoint name="callHierarchyProvider" beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
<with attribute="implementationClass" implements="com.intellij.ide.hierarchy.HierarchyProvider"/>