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
This commit is contained in:
Vladimir Krivosheev
2026-08-10 09:34:26 +00:00
committed by intellij-monorepo-bot
parent d81bf4adb4
commit 76df17c558
6 changed files with 167 additions and 35 deletions
@@ -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
+8 -12
View File
@@ -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
@@ -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>) -> 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 {}
}
@@ -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 {
@@ -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<OpenProjectSettingsService>().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) {
@@ -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()
}
}