mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-91090 Do not throw on corrupt/outdated persistent state value
(cherry picked from commit 34e5cbeb5a938dc7d4f44645e2f99b1540a6c638) IJ-CR-216573 GitOrigin-RevId: 019909a092445fc39b07152e3e1a24505528d9c5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
775bbdc3dc
commit
6fddbd5e13
+18
-1
@@ -4,6 +4,8 @@ package com.intellij.openapi.components
|
||||
import com.intellij.configurationStore.deserialize
|
||||
import com.intellij.configurationStore.jdomSerializer
|
||||
import com.intellij.configurationStore.serializeObjectInto
|
||||
import com.intellij.openapi.diagnostic.rethrowControlFlowException
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.JDOMUtil
|
||||
@@ -17,6 +19,8 @@ import com.intellij.platform.workspace.storage.MutableEntityStorage
|
||||
import com.intellij.util.xmlb.Constants
|
||||
import com.intellij.workspaceModel.ide.legacyBridge.WorkspaceModelLegacyBridge
|
||||
import org.jdom.Element
|
||||
import java.util.concurrent.CancellationException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private const val COMPONENT_ELEMENT: String = "component"
|
||||
|
||||
@@ -26,11 +30,24 @@ internal class CustomImlComponentServiceImpl(
|
||||
|
||||
private val legacyBridge = WorkspaceModelLegacyBridge.getInstance(project)
|
||||
|
||||
/** Guards against log spam: [getComponentValue] re-deserializes on every read, so a corrupt component is reported once. */
|
||||
private val reportedDeserializationFailures = ConcurrentHashMap.newKeySet<String>()
|
||||
|
||||
override fun <T> getComponentValue(module: Module, componentName: String, componentClass: Class<T>): T? {
|
||||
val moduleEntity = legacyBridge.findModuleEntity(module) ?: return null
|
||||
val entity = moduleEntity.customImlComponent ?: return null
|
||||
val component = entity.components[componentName] ?: return null
|
||||
return JDOMUtil.load(component).deserialize(componentClass)
|
||||
// A corrupt or outdated stored value must not break every reader.
|
||||
return try {
|
||||
JDOMUtil.load(component).deserialize(componentClass)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
rethrowControlFlowException(e)
|
||||
if (reportedDeserializationFailures.add("${module.name}#$componentName")) {
|
||||
thisLogger().warn("Cannot deserialize custom iml component '$componentName' for module '${module.name}'; falling back to default", e)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun <T> setComponentValue(module: Module, componentName: String, component: T) {
|
||||
|
||||
+33
@@ -20,6 +20,7 @@ import com.intellij.testFramework.junit5.TestDisposable
|
||||
import com.intellij.testFramework.registerOrReplaceServiceInstance
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.RegisterExtension
|
||||
import java.nio.file.Files
|
||||
@@ -150,6 +151,32 @@ class CustomImlComponentServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null instead of throwing when stored component cannot be deserialized`() {
|
||||
registerContributor(SecondTestModuleService.COMPONENT_NAME, disposable)
|
||||
|
||||
val projectPath = tempDir.newPath("corrupt-component-project")
|
||||
Files.createDirectories(projectPath)
|
||||
|
||||
openProject(projectPath) { project, _ ->
|
||||
runWriteAction {
|
||||
ModuleManager.getInstance(project).newModule(projectPath.resolve(MODULE_FILE_NAME), EmptyModuleType.EMPTY_MODULE)
|
||||
}
|
||||
saveProject(project)
|
||||
}
|
||||
|
||||
openProject(projectPath) { project, _ ->
|
||||
val module = ModuleManager.getInstance(project).modules.single()
|
||||
val componentService = project.service<CustomImlComponentService>()
|
||||
|
||||
runBlocking {
|
||||
componentService.setComponentValue(module, SecondTestModuleService.COMPONENT_NAME, CorruptTestState("not-a-number"))
|
||||
}
|
||||
|
||||
assertNull(componentService.getComponentValue<SecondTestModuleService.State>(module, SecondTestModuleService.COMPONENT_NAME))
|
||||
}
|
||||
}
|
||||
|
||||
private fun registerContributor(componentName: String, disposable: Disposable) {
|
||||
BaseIdeSerializationContext.CUSTOM_IML_COMPONENT_NAME_CONTRIBUTOR_EP.point
|
||||
.registerExtension(TestCustomImlComponentNameContributor(componentName), disposable)
|
||||
@@ -194,6 +221,12 @@ private class TestCustomImlComponentNameContributor(
|
||||
override val componentName: String,
|
||||
) : CustomImlComponentNameContributor
|
||||
|
||||
/**
|
||||
* Serializes a `count` that is not an `Int`, so deserializing it back into [SecondTestModuleService.State]
|
||||
* (whose `count` is an `Int`) fails — mimicking a stored value incompatible with the current state schema.
|
||||
*/
|
||||
internal data class CorruptTestState(var count: String = "")
|
||||
|
||||
internal class FirstTestModuleService(private val module: Module) {
|
||||
data class State(
|
||||
var value: String = "",
|
||||
|
||||
+7
-14
@@ -11,6 +11,7 @@ import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.impl.source.PsiFileImpl;
|
||||
import com.intellij.psi.stubs.StubElement;
|
||||
import com.intellij.util.xmlb.annotations.OptionTag;
|
||||
import com.intellij.util.xmlb.annotations.Transient;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.ast.PyAstClass;
|
||||
import com.jetbrains.python.ast.PyAstElsePart;
|
||||
@@ -186,20 +187,12 @@ public abstract class PyDocumentationSettings
|
||||
return myDocStringFormat;
|
||||
}
|
||||
|
||||
public void setFormat(@NotNull DocStringFormat format) {
|
||||
myDocStringFormat = format;
|
||||
}
|
||||
|
||||
// Legacy name of the field to preserve settings format
|
||||
@SuppressWarnings("unused")
|
||||
@OptionTag("myDocStringFormat")
|
||||
public @NotNull String getFormatName() {
|
||||
return myDocStringFormat.getName();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setFormatName(@NotNull String name) {
|
||||
myDocStringFormat = DocStringFormat.fromNameOrPlain(name);
|
||||
public void setFormat(@Nullable DocStringFormat format) {
|
||||
if (format == null) {
|
||||
myDocStringFormat = DocStringFormat.PLAIN;
|
||||
} else {
|
||||
myDocStringFormat = format;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.python.junit5Tests.unit.alsoWin
|
||||
|
||||
import com.intellij.openapi.util.JDOMUtil
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.util.xmlb.XmlSerializer
|
||||
import com.jetbrains.python.documentation.PyDocumentationSettings.ServiceState
|
||||
import com.jetbrains.python.documentation.docstrings.DocStringFormat
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* Regression test for PY-91090: a stored docstring-format value that no valid build recognizes anymore
|
||||
* (e.g. `format="EPYTEXT"`, removed by PY-52574) must not break deserialization of [ServiceState].
|
||||
*/
|
||||
@TestApplication
|
||||
internal class PyDocumentationSettingsSerializationTest {
|
||||
@Test
|
||||
fun `legacy state with removed docstring format deserializes to default instead of failing`() {
|
||||
// Component as written by an older build: the removed EPYTEXT enum constant leaked into the "format" option
|
||||
// (from the getFormat()/setFormat() accessor pair). Reading it used to call setFormat(null) and throw.
|
||||
val element = JDOMUtil.load(
|
||||
"""
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="analyzeDoctest" value="false" />
|
||||
<option name="renderExternalDocumentation" value="true" />
|
||||
<option name="myDocStringFormat" value="Epytext" />
|
||||
<option name="format" value="EPYTEXT" />
|
||||
</component>
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
val state = XmlSerializer.deserialize(element, ServiceState::class.java)
|
||||
|
||||
// The removed value falls back to PLAIN via setFormatName(); the redundant "format" option is ignored.
|
||||
assertEquals(DocStringFormat.PLAIN, state.format)
|
||||
assertFalse(state.myAnalyzeDoctest)
|
||||
assertTrue(state.myRenderExternalDocumentation)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `format is no longer serialized redundantly`() {
|
||||
val state = ServiceState(DocStringFormat.NUMPY)
|
||||
|
||||
val serialized = JDOMUtil.write(XmlSerializer.serialize(state))
|
||||
|
||||
// Only the legacy "myDocStringFormat" option persists the format; the fragile "format" option is gone.
|
||||
assertTrue(serialized.contains("myDocStringFormat"), serialized)
|
||||
assertFalse(serialized.contains("name=\"format\""), serialized)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user