From 20d1b01a3e7f1528b3453213cc142b8786dca034 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Tue, 30 Jun 2026 18:24:43 +0200 Subject: [PATCH] IJPL-248901 identify ProjectModel code-insight contexts by symbolic id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ModuleContextImpl/LibraryContextImpl/SdkContextImpl based equals/hashCode (and resolution) on the raw EntityPointer. An EntityPointer's id encodes the entity's slot in the current WorkspaceModel storage (arrayId in the high 32 bits), so the same module/library/SDK resolved against two storage generations produces pointers that are not equal. After a full storage replace (common on the remote-dev backend during project-model sync) the editor's cached context and the file's recomputed context referred to the same module yet compared unequal, producing "PsiFile's context does not match the context of the editor" with two ModuleContextImpl values that differed only by EntityPointer id (e.g. 82 vs 4294967378 = 2^32 + 82 — same module class, different slot). Store the entity's stable symbolic id (ModuleId/LibraryId/SdkId) instead and use it for equals/hashCode and for resolution (storage.resolve(symbolicId)). Identity is now generation-independent, so the two sides compare equal again; as a side benefit contexts become stable map keys and resolution no longer breaks when a context outlives the storage generation it was created in. GitOrigin-RevId: f6638bc8feacbb0d3f54ee54efb88d20d46726ef --- .../daemon/impl/MultiverseHighlightingTest.kt | 36 +++++++++++++++ .../ProjectModelContextBridgeImpl.kt | 6 +-- .../ProjectModelEntityContextProvider.kt | 44 +++++++++---------- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/MultiverseHighlightingTest.kt b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/MultiverseHighlightingTest.kt index e8e285c28da5..f4bf11a6548b 100644 --- a/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/MultiverseHighlightingTest.kt +++ b/java/java-tests/testSrc/com/intellij/codeInsight/daemon/impl/MultiverseHighlightingTest.kt @@ -16,11 +16,18 @@ import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.LocalInspectionToolSession import com.intellij.codeInspection.ProblemsHolder import com.intellij.ide.highlighter.JavaFileType +import com.intellij.multiverse.LibraryContextImpl +import com.intellij.multiverse.ModuleContextImpl +import com.intellij.multiverse.SdkContextImpl import com.intellij.openapi.module.ModuleType import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.rootManager +import com.intellij.platform.workspace.jps.entities.LibraryId +import com.intellij.platform.workspace.jps.entities.LibraryTableId +import com.intellij.platform.workspace.jps.entities.ModuleId +import com.intellij.platform.workspace.jps.entities.SdkId import com.intellij.psi.PsiComment import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile @@ -167,6 +174,35 @@ class MultiverseHighlightingTest : DaemonAnalyzerTestCase() { } } + // IJPL-248901: ProjectModel contexts must be identified by their stable symbolic id + // (ModuleId / LibraryId / SdkId), NOT by the EntityPointer. An EntityPointer's id encodes the + // entity's slot in the current WorkspaceModel storage (arrayId in the high 32 bits), so the same + // module resolved against two storage generations (e.g. after a full storage replace on the + // remote-dev backend) yields pointers that are not equal. That made the editor's context and the + // file's context for the same module compare unequal and triggered + // "PsiFile's context does not match the context of the editor" with two ModuleContextImpl values + // that differed only by EntityPointer id. Keying on the symbolic id makes them generation-stable. + fun testProjectModelContextsAreIdentifiedBySymbolicId() { + val module = ModuleContextImpl(ModuleId("m"), project) + val sameModule = ModuleContextImpl(ModuleId("m"), project) + assertEquals(module, sameModule) + assertEquals(module.hashCode(), sameModule.hashCode()) + assertFalse("different modules must not be equal", module == ModuleContextImpl(ModuleId("other"), project)) + + val tableId = LibraryTableId.ProjectLibraryTableId + val library = LibraryContextImpl(LibraryId("lib", tableId), project) + val sameLibrary = LibraryContextImpl(LibraryId("lib", tableId), project) + assertEquals(library, sameLibrary) + assertEquals(library.hashCode(), sameLibrary.hashCode()) + assertFalse("different libraries must not be equal", library == LibraryContextImpl(LibraryId("other", tableId), project)) + + val sdk = SdkContextImpl(SdkId("sdk", "JavaSDK"), project) + val sameSdk = SdkContextImpl(SdkId("sdk", "JavaSDK"), project) + assertEquals(sdk, sameSdk) + assertEquals(sdk.hashCode(), sameSdk.hashCode()) + assertFalse("different SDKs must not be equal", sdk == SdkContextImpl(SdkId("other", "JavaSDK"), project)) + } + private fun getContexts(): List { val contexts = CodeInsightContextManager.getInstance(project).getCodeInsightContexts(virtualFile) assertTrue(contexts.toString(), contexts.all { it is ModuleContext }) diff --git a/platform/lang-impl/src/com/intellij/codeInsight/multiverse/ProjectModelContextBridgeImpl.kt b/platform/lang-impl/src/com/intellij/codeInsight/multiverse/ProjectModelContextBridgeImpl.kt index 313a7e6baecf..484657f9e648 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/multiverse/ProjectModelContextBridgeImpl.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/multiverse/ProjectModelContextBridgeImpl.kt @@ -23,20 +23,20 @@ internal class ProjectModelContextBridgeImpl(private val project: Project) : Pro private val sdkCachedValue = CachedValueWithParameter { storage, parameter -> val sdkEntity = storage.findSdkEntity(parameter) - val context = sdkEntity?.let { SdkContextImpl(sdkEntity.createPointer(), project) } + val context = sdkEntity?.let { SdkContextImpl(sdkEntity.symbolicId, project) } context ?: NULL } override fun getContext(entry: Module): ModuleContext? { val snapshot = currentSnapshot val moduleEntity = entry.findModuleEntity(snapshot) ?: return null - return ModuleContextImpl(moduleEntity.createPointer(), project) + return ModuleContextImpl(moduleEntity.symbolicId, project) } override fun getContext(entry: Library): LibraryContext? { val snapshot = currentSnapshot val lib = snapshot.findLibraryEntity(entry as LibraryBridge) ?: return null - return LibraryContextImpl(lib.createPointer(), project) + return LibraryContextImpl(lib.symbolicId, project) } override fun getContext(entry: Sdk): SdkContext? { diff --git a/platform/projectModel-impl/src/com/intellij/multiverse/ProjectModelEntityContextProvider.kt b/platform/projectModel-impl/src/com/intellij/multiverse/ProjectModelEntityContextProvider.kt index 56549d2ed1cf..23d22b77a4bf 100644 --- a/platform/projectModel-impl/src/com/intellij/multiverse/ProjectModelEntityContextProvider.kt +++ b/platform/projectModel-impl/src/com/intellij/multiverse/ProjectModelEntityContextProvider.kt @@ -16,8 +16,10 @@ import com.intellij.platform.backend.workspace.WorkspaceModelChangeListener import com.intellij.platform.backend.workspace.WorkspaceModelTopics import com.intellij.platform.workspace.jps.entities.ContentRootEntity import com.intellij.platform.workspace.jps.entities.LibraryEntity -import com.intellij.platform.workspace.jps.entities.ModuleEntity +import com.intellij.platform.workspace.jps.entities.LibraryId +import com.intellij.platform.workspace.jps.entities.ModuleId import com.intellij.platform.workspace.jps.entities.SdkEntity +import com.intellij.platform.workspace.jps.entities.SdkId import com.intellij.platform.workspace.jps.entities.SourceRootEntity import com.intellij.platform.workspace.storage.EntityPointer import com.intellij.platform.workspace.storage.ImmutableEntityStorage @@ -79,21 +81,19 @@ internal class ProjectModelEntityContextProvider : CodeInsightContextProvider { val entity = entityPointer.resolve(storage) ?: return null if (entity is SourceRootEntity) { - val modulePointer = entity.contentRoot.module.createPointer() - return ModuleContextImpl(modulePointer, project) + return ModuleContextImpl(entity.contentRoot.module.symbolicId, project) } if (entity is ContentRootEntity) { - val modulePointer = entity.module.createPointer() - return ModuleContextImpl(modulePointer, project) + return ModuleContextImpl(entity.module.symbolicId, project) } if (entity is LibraryEntity) { - return LibraryContextImpl(entity.createPointer(), project) + return LibraryContextImpl(entity.symbolicId, project) } if (entity is SdkEntity) { - return SdkContextImpl(entity.createPointer(), project) + return SdkContextImpl(entity.symbolicId, project) } return null @@ -117,68 +117,68 @@ internal class ProjectModelEntityContextProvider : CodeInsightContextProvider { @ApiStatus.Internal class ModuleContextImpl( - private val modulePointer: EntityPointer, + private val moduleId: ModuleId, private val project: Project ) : ModuleContext { override fun getModule(): Module? { val storage = WorkspaceModel.getInstance(project).currentSnapshot - val entity = modulePointer.resolve(storage) ?: return null + val entity = storage.resolve(moduleId) ?: return null return entity.findModule(storage) } override fun equals(other: Any?): Boolean { - return modulePointer == (other as? ModuleContextImpl)?.modulePointer + return moduleId == (other as? ModuleContextImpl)?.moduleId } override fun hashCode(): Int { - return modulePointer.hashCode() + return moduleId.hashCode() } - override fun toString(): String = "ModuleContextImpl(modulePointer=$modulePointer, project=${project.name})" + override fun toString(): String = "ModuleContextImpl(moduleId=$moduleId, project=${project.name})" } @ApiStatus.Internal class LibraryContextImpl( - private val libraryPointer: EntityPointer, + private val libraryId: LibraryId, private val project: Project, ) : LibraryContext { override fun getLibrary(): Library? { val storage = WorkspaceModel.getInstance(project).currentSnapshot - val entity = libraryPointer.resolve(storage) ?: return null + val entity = storage.resolve(libraryId) ?: return null return entity.findLibraryBridge(storage) } override fun equals(other: Any?): Boolean { - return libraryPointer == (other as? LibraryContextImpl)?.libraryPointer + return libraryId == (other as? LibraryContextImpl)?.libraryId } override fun hashCode(): Int { - return libraryPointer.hashCode() + return libraryId.hashCode() } - override fun toString(): String = "LibraryContextImpl(libraryPointer=$libraryPointer, project=${project.name})" + override fun toString(): String = "LibraryContextImpl(libraryId=$libraryId, project=${project.name})" } @ApiStatus.Internal class SdkContextImpl( - private val sdkPointer: EntityPointer, + private val sdkId: SdkId, private val project: Project, ) : SdkContext { override fun getSdk(): Sdk? { val storage = WorkspaceModel.getInstance(project).currentSnapshot - val entity = sdkPointer.resolve(storage) ?: return null + val entity = storage.resolve(sdkId) ?: return null return storage.findSdk(entity) } override fun equals(other: Any?): Boolean { - return sdkPointer == (other as? SdkContextImpl)?.sdkPointer + return sdkId == (other as? SdkContextImpl)?.sdkId } override fun hashCode(): Int { - return sdkPointer.hashCode() + return sdkId.hashCode() } - override fun toString(): String = "SdkContextImpl(sdkPointer=$sdkPointer, project=${project.name})" + override fun toString(): String = "SdkContextImpl(sdkId=$sdkId, project=${project.name})" }