IJPL-248901 identify ProjectModel code-insight contexts by symbolic id

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
This commit is contained in:
Max Medvedev
2026-08-12 16:47:57 +00:00
committed by intellij-monorepo-bot
parent d77fbccddc
commit 20d1b01a3e
3 changed files with 61 additions and 25 deletions
@@ -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<ModuleContext> {
val contexts = CodeInsightContextManager.getInstance(project).getCodeInsightContexts(virtualFile)
assertTrue(contexts.toString(), contexts.all { it is ModuleContext })
@@ -23,20 +23,20 @@ internal class ProjectModelContextBridgeImpl(private val project: Project) : Pro
private val sdkCachedValue = CachedValueWithParameter<Sdk, SdkContext> { 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? {
@@ -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<ModuleEntity>()
return ModuleContextImpl(modulePointer, project)
return ModuleContextImpl(entity.contentRoot.module.symbolicId, project)
}
if (entity is ContentRootEntity) {
val modulePointer = entity.module.createPointer<ModuleEntity>()
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<ModuleEntity>,
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<LibraryEntity>,
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<SdkEntity>,
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})"
}