From 76df17c55801e7ddbd9b7644310ccadeb8af43d0 Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Thu, 6 Aug 2026 17:32:12 +0200 Subject: [PATCH] IJPL-252216 preserve OpenProjectTask binary compatibility Keep the generated data-class ABI stable and route plugin construction through the non-inline builder boundary. Store the post-open file marker in implementation options while retaining compatibility getters and the transient constructor shape. (cherry picked from commit 40094a325e760b8afbd9a82fc7fdee762dc89927) IJ-MR-217434 GitOrigin-RevId: 6faee079e1da873a574433ec646d16231f7f9bb5 --- .../ide-core-impl/api-dump-unreviewed.txt | 3 + platform/ide-core-impl/api-dump.txt | 20 ++- .../com/intellij/ide/impl/OpenProjectTask.kt | 121 ++++++++++++++++-- .../intellij/ide/RecentProjectsManagerBase.kt | 13 +- .../platform/PlatformProjectOpenProcessor.kt | 11 +- .../intellij/ide/impl/OpenProjectTaskTest.kt | 34 +++++ 6 files changed, 167 insertions(+), 35 deletions(-) create mode 100644 platform/platform-tests/testSrc/com/intellij/ide/impl/OpenProjectTaskTest.kt diff --git a/platform/ide-core-impl/api-dump-unreviewed.txt b/platform/ide-core-impl/api-dump-unreviewed.txt index 31b253ef333a..e24bb53285db 100644 --- a/platform/ide-core-impl/api-dump-unreviewed.txt +++ b/platform/ide-core-impl/api-dump-unreviewed.txt @@ -65,7 +65,10 @@ f:com.intellij.ide.impl.OpenProjectTask - f:component12():Z - f:component13():Z - f:component14():Z +- f:component15():java.lang.String +- f:component17():Z - f:component2():Z +- f:component20():kotlin.jvm.functions.Function2 - f:component3():com.intellij.openapi.project.Project - f:component4():Z - f:component5():Z diff --git a/platform/ide-core-impl/api-dump.txt b/platform/ide-core-impl/api-dump.txt index dbddf1d91265..c7ecf71388e8 100644 --- a/platform/ide-core-impl/api-dump.txt +++ b/platform/ide-core-impl/api-dump.txt @@ -85,21 +85,17 @@ c:com.intellij.ide.impl.DataManagerImpl f:com.intellij.ide.impl.OpenProjectTask - f:asNewProject():com.intellij.ide.impl.OpenProjectTask - sf:build():com.intellij.ide.impl.OpenProjectTask -- f:component15():Z - f:component16():java.lang.String -- f:component17():java.lang.String - f:component18():Z -- f:component19():Z -- f:component20():kotlin.jvm.functions.Function1 +- f:component19():kotlin.jvm.functions.Function1 - f:component21():kotlin.jvm.functions.Function2 -- f:component22():kotlin.jvm.functions.Function2 -- f:component23():Z -- f:component24():kotlin.jvm.functions.Function1 -- f:component25():java.lang.Object -- f:component26():java.nio.file.Path -- f:component27():Z -- f:copy(Z,Z,com.intellij.openapi.project.Project,Z,Z,com.intellij.openapi.project.Project,java.lang.String,Z,com.intellij.projectImport.ProjectOpenedCallback,I,I,Z,Z,Z,Z,java.lang.String,java.lang.String,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,Z,kotlin.jvm.functions.Function1,java.lang.Object,java.nio.file.Path,Z):com.intellij.ide.impl.OpenProjectTask -- bs:copy$default(com.intellij.ide.impl.OpenProjectTask,Z,Z,com.intellij.openapi.project.Project,Z,Z,com.intellij.openapi.project.Project,java.lang.String,Z,com.intellij.projectImport.ProjectOpenedCallback,I,I,Z,Z,Z,Z,java.lang.String,java.lang.String,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,Z,kotlin.jvm.functions.Function1,java.lang.Object,java.nio.file.Path,Z,I,java.lang.Object):com.intellij.ide.impl.OpenProjectTask +- f:component22():Z +- f:component23():kotlin.jvm.functions.Function1 +- f:component24():java.lang.Object +- f:component25():java.nio.file.Path +- f:component26():Z +- f:copy(Z,Z,com.intellij.openapi.project.Project,Z,Z,com.intellij.openapi.project.Project,java.lang.String,Z,com.intellij.projectImport.ProjectOpenedCallback,I,I,Z,Z,Z,java.lang.String,java.lang.String,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,Z,kotlin.jvm.functions.Function1,java.lang.Object,java.nio.file.Path,Z):com.intellij.ide.impl.OpenProjectTask +- bs:copy$default(com.intellij.ide.impl.OpenProjectTask,Z,Z,com.intellij.openapi.project.Project,Z,Z,com.intellij.openapi.project.Project,java.lang.String,Z,com.intellij.projectImport.ProjectOpenedCallback,I,I,Z,Z,Z,java.lang.String,java.lang.String,Z,Z,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function2,kotlin.jvm.functions.Function2,Z,kotlin.jvm.functions.Function1,java.lang.Object,java.nio.file.Path,Z,I,java.lang.Object):com.intellij.ide.impl.OpenProjectTask - f:getBeforeInit():kotlin.jvm.functions.Function1 - f:getBeforeOpen():kotlin.jvm.functions.Function2 - f:getCallback():com.intellij.projectImport.ProjectOpenedCallback diff --git a/platform/ide-core-impl/src/com/intellij/ide/impl/OpenProjectTask.kt b/platform/ide-core-impl/src/com/intellij/ide/impl/OpenProjectTask.kt index 08386aa22d37..27a5673b41ca 100644 --- a/platform/ide-core-impl/src/com/intellij/ide/impl/OpenProjectTask.kt +++ b/platform/ide-core-impl/src/com/intellij/ide/impl/OpenProjectTask.kt @@ -11,6 +11,19 @@ import org.jetbrains.annotations.TestOnly import java.nio.file.Path import java.util.function.Predicate +private data class OpenProjectTaskImplOptions( + @JvmField val delegate: Any?, + @JvmField val opensFileAfterProjectOpen: Boolean, +) + +private fun Any?.asOpenProjectTaskImplOptions(): OpenProjectTaskImplOptions { + return this as? OpenProjectTaskImplOptions ?: OpenProjectTaskImplOptions(delegate = this, opensFileAfterProjectOpen = false) +} + +private fun Any?.withOpensFileAfterProjectOpen(value: Boolean): OpenProjectTaskImplOptions { + return asOpenProjectTaskImplOptions().copy(opensFileAfterProjectOpen = value) +} + data class OpenProjectTask @Internal constructor( val forceOpenInNewFrame: Boolean, val forceReuseFrame: Boolean = false, @@ -26,15 +39,6 @@ data class OpenProjectTask @Internal constructor( val callback: ProjectOpenedCallback?, val line: Int, val column: Int, - /** - * Whether whoever opens this project is going to open an editor of its own once opening has finished — a file named on the command - * line, for instance. - * - * The editor area holds back what it would otherwise show while project open is still deciding what goes there, and it can only - * hold back what it knows about; this reports work that outlives project open itself. - */ - @Internal - val opensFileAfterProjectOpen: Boolean = false, @Deprecated("Not used") val isRefreshVfsNeeded: Boolean, /** @@ -66,6 +70,80 @@ data class OpenProjectTask @Internal constructor( @Internal val createModule: Boolean, ) { + /** + * Whether whoever opens this project is going to open an editor of its own once opening has finished — a file named on the command + * line, for instance. + * + * The editor area holds back what it would otherwise show while project open is still deciding what goes there, and it can only + * hold back what it knows about; this reports work that outlives project open itself. + * + * This property is deliberately not a primary-constructor parameter: adding one changes the generated data-class ABI. + */ + @get:Internal + val opensFileAfterProjectOpen: Boolean + get() = (implOptions as? OpenProjectTaskImplOptions)?.opensFileAfterProjectOpen == true + + /** + * Compatibility bridge for plugins compiled against builds where [opensFileAfterProjectOpen] was a primary-constructor parameter. + */ + @Internal + constructor( + forceOpenInNewFrame: Boolean, + forceReuseFrame: Boolean, + projectToClose: Project?, + isNewProject: Boolean, + useDefaultProjectAsTemplate: Boolean, + project: Project?, + projectName: String?, + showWelcomeScreen: Boolean, + callback: ProjectOpenedCallback?, + line: Int, + column: Int, + opensFileAfterProjectOpen: Boolean, + isRefreshVfsNeeded: Boolean, + runConfigurators: Boolean, + runConversionBeforeOpen: Boolean, + projectWorkspaceId: String?, + projectFrameTypeId: String?, + isProjectCreatedWithWizard: Boolean, + preloadServices: Boolean, + beforeInit: ((Project) -> Unit)?, + beforeOpen: (suspend (Project) -> Boolean)?, + preparedToOpen: (suspend (Module) -> Unit)?, + preventIprLookup: Boolean, + processorChooser: ((List) -> Any)?, + implOptions: Any?, + projectRootDir: Path?, + createModule: Boolean, + ) : this( + forceOpenInNewFrame = forceOpenInNewFrame, + forceReuseFrame = forceReuseFrame, + projectToClose = projectToClose, + isNewProject = isNewProject, + useDefaultProjectAsTemplate = useDefaultProjectAsTemplate, + project = project, + projectName = projectName, + showWelcomeScreen = showWelcomeScreen, + callback = callback, + line = line, + column = column, + isRefreshVfsNeeded = isRefreshVfsNeeded, + runConfigurators = runConfigurators, + runConversionBeforeOpen = runConversionBeforeOpen, + projectWorkspaceId = projectWorkspaceId, + projectFrameTypeId = projectFrameTypeId, + isProjectCreatedWithWizard = isProjectCreatedWithWizard, + preloadServices = preloadServices, + beforeInit = beforeInit, + beforeOpen = beforeOpen, + preparedToOpen = preparedToOpen, + preventIprLookup = preventIprLookup, + processorChooser = processorChooser, + implOptions = implOptions.withOpensFileAfterProjectOpen(opensFileAfterProjectOpen), + projectRootDir = projectRootDir, + createModule = createModule, + ) + @Internal constructor( forceOpenInNewFrame: Boolean = false, @@ -102,7 +180,7 @@ data class OpenProjectTask @Internal constructor( preparedToOpen = null, processorChooser = null, - implOptions = null, + implOptions = OpenProjectTaskImplOptions(delegate = null, opensFileAfterProjectOpen = false), createModule = true, projectRootDir = null, @@ -118,6 +196,22 @@ data class OpenProjectTask @Internal constructor( fun asNewProject(): OpenProjectTask = copy(isNewProject = true, useDefaultProjectAsTemplate = true) fun withProject(project: Project?): OpenProjectTask = copy(project = project) fun withProjectName(projectName: String?): OpenProjectTask = copy(projectName = projectName) + + @Internal + fun markAsOpeningFileAfterProjectOpen(): OpenProjectTask = copy(implOptions = implOptions.withOpensFileAfterProjectOpen(value = true)) +} + +@get:Internal +val OpenProjectTask.effectiveImplOptions: Any? + get() = when (val options = implOptions) { + is OpenProjectTaskImplOptions -> options.delegate + else -> options + } + +@Internal +fun OpenProjectTask.withImplOptions(implOptions: Any?): OpenProjectTask { + val options = this.implOptions.asOpenProjectTaskImplOptions().copy(delegate = implOptions) + return copy(implOptions = options) } class OpenProjectTaskBuilder @PublishedApi internal constructor() { @@ -186,7 +280,7 @@ class OpenProjectTaskBuilder @PublishedApi internal constructor() { createModule = false } - @PublishedApi internal inline fun build(builder: OpenProjectTaskBuilder.() -> Unit): OpenProjectTask { + @PublishedApi internal fun build(builder: OpenProjectTaskBuilder.() -> Unit): OpenProjectTask { builder() if (project != null && createModule) { thisLogger().warn("Project is explicitly set (name=${project?.name}), but createModule is true") @@ -232,5 +326,8 @@ class OpenProjectTaskBuilder @PublishedApi internal constructor() { @Internal inline fun OpenProjectTask(buildAction: OpenProjectTaskBuilder.() -> Unit): OpenProjectTask { - return OpenProjectTaskBuilder().build(buildAction) + val builder = OpenProjectTaskBuilder() + builder.buildAction() + // Keep the pre-existing build(Function1) call in client bytecode, but do not inline the builder implementation into clients. + return builder.build {} } diff --git a/platform/platform-impl/src/com/intellij/ide/RecentProjectsManagerBase.kt b/platform/platform-impl/src/com/intellij/ide/RecentProjectsManagerBase.kt index f2f0f2d20dcc..a3d9aafe7c78 100644 --- a/platform/platform-impl/src/com/intellij/ide/RecentProjectsManagerBase.kt +++ b/platform/platform-impl/src/com/intellij/ide/RecentProjectsManagerBase.kt @@ -9,6 +9,8 @@ import com.intellij.ide.impl.OpenProjectTask import com.intellij.ide.impl.ProjectUtil import com.intellij.ide.impl.ProjectUtil.isSameProject import com.intellij.ide.impl.ProjectUtilService +import com.intellij.ide.impl.effectiveImplOptions +import com.intellij.ide.impl.withImplOptions import com.intellij.ide.lightEdit.LightEdit import com.intellij.idea.AppMode import com.intellij.openapi.actionSystem.AnAction @@ -461,13 +463,12 @@ open class RecentProjectsManagerBase(coroutineScope: CoroutineScope) : open suspend fun openProject(projectFile: Path, options: OpenProjectTask): Project? { var effectiveOptions = options - if (options.implOptions == null) { + if (options.effectiveImplOptions == null) { getProjectMetaInfo(projectFile)?.let { info -> effectiveOptions = effectiveOptions.copy( projectWorkspaceId = info.projectWorkspaceId, projectFrameTypeId = info.projectFrameTypeId, - implOptions = OpenProjectImplOptions(recentProjectMetaInfo = info, frameInfo = info.frame) - ) + ).withImplOptions(OpenProjectImplOptions(recentProjectMetaInfo = info, frameInfo = info.frame)) } } @@ -1195,13 +1196,13 @@ data class OpenProjectImplOptions( ) val OpenProjectTask.frame: IdeFrameImpl? - @Internal get() = (implOptions as OpenProjectImplOptions?)?.frame + @Internal get() = (effectiveImplOptions as OpenProjectImplOptions?)?.frame val OpenProjectTask.frameInfo: FrameInfo? - @Internal get() = (implOptions as OpenProjectImplOptions?)?.frameInfo + @Internal get() = (effectiveImplOptions as OpenProjectImplOptions?)?.frameInfo val OpenProjectTask.recentProjectMetaInfo: RecentProjectMetaInfo? - @Internal get() = (implOptions as OpenProjectImplOptions?)?.recentProjectMetaInfo + @Internal get() = (effectiveImplOptions as OpenProjectImplOptions?)?.recentProjectMetaInfo @Internal interface SystemDock { diff --git a/platform/platform-impl/src/com/intellij/platform/PlatformProjectOpenProcessor.kt b/platform/platform-impl/src/com/intellij/platform/PlatformProjectOpenProcessor.kt index 9ffdb696d210..df99db92e67f 100644 --- a/platform/platform-impl/src/com/intellij/platform/PlatformProjectOpenProcessor.kt +++ b/platform/platform-impl/src/com/intellij/platform/PlatformProjectOpenProcessor.kt @@ -122,13 +122,14 @@ class PlatformProjectOpenProcessor : ProjectOpenProcessor(), CommandLineProjectO projectName = dummyProjectName, runConfigurators = false, runConversionBeforeOpen = false, - // both callers of this go on to `openFileFromCommandLine`, which is what releases the hold this asks for - opensFileAfterProjectOpen = true, beforeOpen = { project -> project.service().state.isLocatedInTempDirectory = true options.beforeOpen?.invoke(project) ?: true } - ) + ).let { + // both callers of this go on to `openFileFromCommandLine`, which is what releases the hold this asks for + it.markAsOpeningFileAfterProjectOpen() + } } private fun createTempProjectAndOpenFile(file: Path, options: OpenProjectTask): Project? { @@ -216,7 +217,7 @@ class PlatformProjectOpenProcessor : ProjectOpenProcessor(), CommandLineProjectO options } else { - options.copy(projectName = file.fileName.toString(), opensFileAfterProjectOpen = true) + options.copy(projectName = file.fileName.toString()).markAsOpeningFileAfterProjectOpen() } ) if (project != null && file != baseDir) { @@ -290,7 +291,7 @@ class PlatformProjectOpenProcessor : ProjectOpenProcessor(), CommandLineProjectO options } else { - options.copy(projectName = file.fileName.toString(), opensFileAfterProjectOpen = true) + options.copy(projectName = file.fileName.toString()).markAsOpeningFileAfterProjectOpen() } ) if (project != null && file != baseDir) { diff --git a/platform/platform-tests/testSrc/com/intellij/ide/impl/OpenProjectTaskTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/impl/OpenProjectTaskTest.kt new file mode 100644 index 000000000000..2e6c9efde219 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/ide/impl/OpenProjectTaskTest.kt @@ -0,0 +1,34 @@ +// 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.ide.impl + +import com.intellij.testFramework.assertions.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class OpenProjectTaskTest { + @Test + fun `implementation options preserve their payload and file marker`() { + val originalImplOptions = Any() + val replacementImplOptions = Any() + val options = OpenProjectTask { + implOptions = originalImplOptions + opensFileAfterProjectOpen = true + } + + assertThat(options.implOptions).isNotSameAs(originalImplOptions) + assertThat(options.effectiveImplOptions).isSameAs(originalImplOptions) + assertThat(options.opensFileAfterProjectOpen).isTrue() + + val copiedOptions = options.copy(projectName = "copied") + assertThat(copiedOptions.effectiveImplOptions).isSameAs(originalImplOptions) + assertThat(copiedOptions.opensFileAfterProjectOpen).isTrue() + + val replacedOptions = copiedOptions.withImplOptions(replacementImplOptions) + assertThat(replacedOptions.effectiveImplOptions).isSameAs(replacementImplOptions) + assertThat(replacedOptions.opensFileAfterProjectOpen).isTrue() + + val defaultOptions = OpenProjectTask() + assertThat(defaultOptions.implOptions).isNotNull() + assertThat(defaultOptions.effectiveImplOptions).isNull() + assertThat(defaultOptions.opensFileAfterProjectOpen).isFalse() + } +}