diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/CommunityPluginModelTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/CommunityPluginModelTest.kt index f68d08da0a7f..16ae62b219a6 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/CommunityPluginModelTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/CommunityPluginModelTest.kt @@ -13,6 +13,9 @@ class CommunityPluginModelTest { val communityPath = Path.of(PlatformTestUtil.getCommunityPath()) val options = PluginValidationOptions( skipUnresolvedOptionalContentModules = true, + // There are a number of platform services that are overridden in ultimate only. Instead of declaring all of them here, we + // only perform the check once in UltimatePluginModelTest + skipServicesOverridesCheck = true, referencedPluginIdsOfExternalPlugins = setOf( //these modules are defined in the ultimate part "com.intellij.marketplace", diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt index 01ef30e2fcfd..6ada7232c33b 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/PluginModelValidator.kt @@ -7,10 +7,7 @@ import com.fasterxml.jackson.core.JsonFactory import com.fasterxml.jackson.core.JsonGenerator import com.intellij.openapi.extensions.PluginId import com.intellij.platform.plugins.parser.impl.RawPluginDescriptor -import com.intellij.platform.plugins.parser.impl.elements.ContentModuleElement -import com.intellij.platform.plugins.parser.impl.elements.DependenciesElement -import com.intellij.platform.plugins.parser.impl.elements.ModuleLoadingRuleValue -import com.intellij.platform.plugins.parser.impl.elements.ModuleVisibilityValue +import com.intellij.platform.plugins.parser.impl.elements.* import com.intellij.platform.plugins.testFramework.LoadFromSourceXIncludeLoader import com.intellij.platform.plugins.testFramework.loadRawPluginDescriptorInTest import com.intellij.project.IntelliJProjectConfiguration @@ -52,6 +49,7 @@ data class PluginVariantWithDynamicIncludes( data class PluginValidationOptions( val skipUnresolvedOptionalContentModules: Boolean = false, + val skipServicesOverridesCheck: Boolean = false, val reportDependsTagInPluginXmlWithPackageAttribute: Boolean = true, val referencedPluginIdsOfExternalPlugins: Set = emptySet(), val prefixesOfPathsIncludedFromLibrariesViaXiInclude: List = emptyList(), @@ -86,6 +84,8 @@ data class PluginValidationOptions( val componentImplementationClassesToIgnore: Set = emptySet(), val pluginVariantsWithDynamicIncludes: List = emptyList(), + + val externallyOverriddenServices: Set = emptySet(), ) fun validatePluginModel(projectPath: Path, validationOptions: PluginValidationOptions = PluginValidationOptions()): PluginValidationResult { @@ -264,12 +264,17 @@ class PluginModelValidator( checkModuleElements(moduleDescriptor = pluginInfo.descriptor, sourceModule = pluginInfo.sourceModule, pluginInfo.descriptorFile) } + // additional content check: services overrides + if (!validationOptions.skipServicesOverridesCheck) { + checkServicesOverrides(descriptorFileInfos) + } + + // 3. check dependencies - we are aware about all modules now val contentModuleToContainingPlugins = HashMap>() for (pluginInfo in allMainModulesOfPlugins) { pluginInfo.content.groupByTo(contentModuleToContainingPlugins, { it.name!! }, { pluginInfo }) } - // 3. check dependencies - we are aware about all modules now for (pluginInfo in allMainModulesOfPlugins) { val descriptor = pluginInfo.descriptor @@ -337,6 +342,93 @@ class PluginModelValidator( return PluginValidationResult(_errors, pluginIdToInfo) } + private fun getOpenServices( + services: List, + descriptor: DescriptorFileInfo, + ): Set { + return services + .filter { it.open } + .mapNotNull { getServiceInterface(it, descriptor) } + .toSet() + } + + private fun getOverriddenServices( + services: List, + descriptor: DescriptorFileInfo, + ): Set { + return services + .filter { it.overrides } + .mapNotNull { getServiceInterface(it, descriptor) } + .toSet() + } + + private fun getServiceInterface(service: ServiceElement, descriptorForLogging: DescriptorFileInfo): String? { + val serviceInterface = service.serviceInterface ?: service.serviceImplementation + if (serviceInterface == null) { + reportError("Services declared must declare `serviceInterfance` or `serviceImplementation`\n" + + "$service in ${descriptorForLogging.descriptorFile}", descriptorForLogging.sourceModule) + } + return serviceInterface + } + + private fun checkServicesOverrides(descriptors: Collection) { + val allOpenAppServices = HashSet() + val allOpenProjectServices = HashSet() + val allOverriddenAppServices = HashSet() + val allOverriddenProjectServices = HashSet() + + for (descriptor in descriptors) { + allOpenProjectServices.addAll(getOpenServices(descriptor.descriptor.projectElementsContainer.services, descriptor)) + allOverriddenProjectServices.addAll(getOverriddenServices(descriptor.descriptor.projectElementsContainer.services, descriptor)) + allOpenAppServices.addAll(getOpenServices(descriptor.descriptor.appElementsContainer.services, descriptor)) + allOverriddenAppServices.addAll(getOverriddenServices(descriptor.descriptor.appElementsContainer.services, descriptor)) + } + + for (descriptor in descriptors) { + checkServicesOverridesInSingleScopedContainer(descriptor, + descriptor.descriptor.appElementsContainer.services, + allOpenAppServices, + allOverriddenAppServices) + checkServicesOverridesInSingleScopedContainer(descriptor, + descriptor.descriptor.projectElementsContainer.services, + allOpenProjectServices, + allOverriddenProjectServices) + } + } + + private fun checkServicesOverridesInSingleScopedContainer( + descriptor: DescriptorFileInfo, + servicesInContainerAndInDescriptor: List, + allOpenServicesInContainer: Set, + allOverriddenServicesInContainer: Set, + ) { + getOverriddenServices(servicesInContainerAndInDescriptor, descriptor) + .filterNot { allOpenServicesInContainer.contains(it) } + .forEach { serviceInterface -> + reportError("Service $serviceInterface is not open for override.\n" + + "Please either add `open='true'` to the service declaration you want to override, or remove `overrides='true'` in ${descriptor.descriptorFile}", + descriptor.sourceModule) + } + + getOpenServices(servicesInContainerAndInDescriptor, descriptor) + .filterNot { allOverriddenServicesInContainer.contains(it) || validationOptions.externallyOverriddenServices.contains(it) } + .forEach { serviceInterface -> + reportError("Service $serviceInterface is declared as open in ${descriptor.descriptorFile}, but is not overridden anywhere.\n" + + "Please consider making the service non-open, or add this service to `externallyOverriddenServices` if some external override exists.", + descriptor.sourceModule) + } + + servicesInContainerAndInDescriptor + .filter { !it.overrides && !it.open } + .mapNotNull { getServiceInterface(it, descriptor) } + .filter { allOpenServicesInContainer.contains(it) } + .forEach { serviceInterface -> + reportError("Service $serviceInterface is declared as open in some plugins, but not in ${descriptor.descriptorFile}.\n" + + "Please add `open='true'` to the your service declaration.", + descriptor.sourceModule) + } + } + private fun checkDepends( pluginInfo: ModuleInfo, descriptor: RawPluginDescriptor,