IJPL-217109: IAE: [WelcomeScreenNewFileDialog.doOKAction]

- Fix "java.lang.IllegalArgumentException: separators must be '/'" on Windows when creating a new file from the Welcome Screen

GitOrigin-RevId: bf5872459ccb17f966f0189c9a3d0be8e102dd07
This commit is contained in:
iliakondratev
2025-11-17 11:38:04 +01:00
committed by intellij-monorepo-bot
parent 011d91dd1f
commit 606636ffce
6 changed files with 171 additions and 5 deletions

View File

@@ -364,6 +364,8 @@ jvm_library(
"//plugins/jsonpath",
"//plugins/jsonpath:jsonpath_test_lib",
"//plugins/ui-designer/jps-plugin/tests:tests_test_lib",
"//platform/non-modal-welcome-screen",
"//platform/non-modal-welcome-screen:non-modal-welcome-screen_test_lib",
"//platform/util/coroutines:coroutines-tests_test_lib",
"//platform/util/progress:progress-tests_test_lib",
"//platform/testFramework/junit5.jimfs",

View File

@@ -217,6 +217,7 @@
<orderEntry type="module" module-name="intellij.java.guiForms.jps.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ide.newUiOnboarding" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.ide.newUsersOnboarding" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.platform.ide.nonModalWelcomeScreen" scope="TEST" />
<orderEntry type="module" module-name="intellij.kotlin.onboarding.promoter" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.execution.process.elevation" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.vcs.gitlab.yaml" scope="RUNTIME" />

View File

@@ -36,4 +36,47 @@ jvm_library(
],
plugins = ["@lib//:compose-plugin"]
)
### auto-generated section `build intellij.platform.ide.nonModalWelcomeScreen` end
jvm_library(
name = "non-modal-welcome-screen_test_lib",
visibility = ["//visibility:public"],
srcs = glob(["testSrc/**/*.kt", "testSrc/**/*.java", "testSrc/**/*.form"], allow_empty = True),
associates = [":non-modal-welcome-screen"],
deps = [
"//platform/analysis-api:analysis",
"//platform/editor-ui-api:editor-ui",
"//platform/compose",
"//platform/compose:compose_test_lib",
"//platform/core-api:core",
"//jps/model-api:model",
"//platform/lang-core",
"//libraries/compose-runtime-desktop",
"//platform/projectModel-api:projectModel",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/util:util-ui",
"//platform/platform-api:ide",
"//platform/lang-impl",
"//platform/core-ui",
"//platform/ide-core-impl",
"//platform/platform-impl:ide-impl",
"@lib//:junit5",
"//platform/statistics",
"//platform/statistics:statistics_test_lib",
"//libraries/kotlinx/serialization/core",
"//platform/kernel/shared:kernel",
"//platform/platform-impl/rpc",
"//platform/project/shared:project",
],
plugins = ["@lib//:compose-plugin"]
)
### auto-generated section `build intellij.platform.ide.nonModalWelcomeScreen` end
### auto-generated section `test intellij.platform.ide.nonModalWelcomeScreen` start
load("@community//build:tests-options.bzl", "jps_test")
jps_test(
name = "non-modal-welcome-screen_test",
runtime_deps = [":non-modal-welcome-screen_test_lib"]
)
### auto-generated section `test intellij.platform.ide.nonModalWelcomeScreen` end

View File

@@ -30,6 +30,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -22,10 +22,12 @@ import com.intellij.util.ui.FormBuilder
import org.jetbrains.annotations.ApiStatus
import java.awt.event.FocusAdapter
import java.awt.event.FocusEvent
import java.nio.file.InvalidPathException
import java.nio.file.Path
import javax.swing.JComponent
import javax.swing.JList
import javax.swing.event.DocumentEvent
import kotlin.io.path.invariantSeparatorsPathString
@ApiStatus.Internal
class WelcomeScreenNewFileDialog private constructor(
@@ -33,8 +35,24 @@ class WelcomeScreenNewFileDialog private constructor(
private val builder: Builder,
) : DialogWrapper(project, true) {
private companion object {
internal companion object {
private const val MAX_PATH_LENGTH = 70
/**
* Normalizes a directory path for use with [DirectoryUtil.mkdirs].
*
* This method ensures the path:
* - Uses forward slashes (/) as separators, which is required by [DirectoryUtil.mkdirs]
* - Has redundant path elements (like `.` and `..`) resolved
*
* This is necessary because on Windows, [Path.toString] returns paths with backslashes,
* but [DirectoryUtil.mkdirs] requires forward slashes.
*
* @see IJPL-217109
*/
fun normalizeDirectoryPath(path: String): String {
return Path.of(path).normalize().invariantSeparatorsPathString
}
}
private val targetDirectoryField: ComponentWithBrowseButton<JBTextField> = ComponentWithBrowseButton(ExtendableTextField(), null)
@@ -155,7 +173,7 @@ class WelcomeScreenNewFileDialog private constructor(
val targetDirectoryName = targetDirectoryField.childComponent.text
if (targetDirectoryName.isEmpty()) {
if (targetDirectoryName.isNullOrEmpty()) {
Messages.showErrorDialog(
project,
NonModalWelcomeScreenBundle.message("welcome.screen.create.file.dialog.no.target.directory.specified"),
@@ -168,12 +186,14 @@ class WelcomeScreenNewFileDialog private constructor(
ApplicationManager.getApplication().runWriteAction {
try {
targetDirectory = DirectoryUtil.mkdirs(
PsiManager.getInstance(project),
Path.of(targetDirectoryName).normalize().toString()
PsiManager.getInstance(project),
normalizeDirectoryPath(targetDirectoryName)
)
}
catch (_: IncorrectOperationException) {
}
catch (_: InvalidPathException) {
}
}
}, NonModalWelcomeScreenBundle.message("welcome.screen.create.file.dialog.create.directory"), null)

View File

@@ -0,0 +1,99 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.ide.nonModalWelcomeScreen.newFileDialog
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.condition.EnabledOnOs
import org.junit.jupiter.api.condition.OS
import java.nio.file.FileSystems
/**
* Tests for [WelcomeScreenNewFileDialog.normalizeDirectoryPath].
*
* The method ensures paths use forward slashes, which is required by [DirectoryUtil.mkdirs].
*
* **Note:** Windows-specific tests use [@EnabledOnOs] and will be skipped on other platforms.
* They will run on Windows agents in TeamCity.
*
* @see IJPL-217109
*/
class WelcomeScreenNewFileDialogTest {
/**
* Regression test for IJPL-217109: [DirectoryUtil.mkdirs] requires forward slashes.
* Simulates user input with platform-native separators (backslashes on Windows).
*/
@Test
fun `normalizeDirectoryPath uses forward slashes`() {
val separator = FileSystems.getDefault().separator
val userInput = listOf("Users", "test", "NewProject").joinToString(separator)
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(userInput)
assertFalse(normalized.contains("\\"),
"Path for DirectoryUtil must not contain backslashes: $normalized")
}
@Test
fun `normalizeDirectoryPath resolves parent directory references`() {
val pathWithDots = "home/user/../user/project"
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithDots)
assertFalse(normalized.contains(".."), "Normalized path should not contain '..': $normalized")
assertEquals("home/user/project", normalized)
}
@Test
fun `normalizeDirectoryPath resolves current directory references`() {
val pathWithDot = "home/./user/./project"
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithDot)
assertEquals("home/user/project", normalized)
}
/**
* Regression test for IJPL-217109: Windows backslash paths must be converted to forward slashes.
* This test runs ONLY on Windows agents in TeamCity.
*/
@Test
@EnabledOnOs(OS.WINDOWS)
fun `normalizeDirectoryPath converts Windows backslashes to forward slashes`() {
val windowsPath = "C:\\Users\\test\\project"
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(windowsPath)
assertEquals("C:/Users/test/project", normalized)
}
/**
* Tests Windows paths with mixed separators (common when copy-pasting paths).
* This test runs ONLY on Windows agents in TeamCity.
*/
@Test
@EnabledOnOs(OS.WINDOWS)
fun `normalizeDirectoryPath handles Windows mixed separators`() {
val mixedPath = "C:/Users\\test/project\\src"
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(mixedPath)
assertEquals("C:/Users/test/project/src", normalized)
assertFalse(normalized.contains("\\"), "Path should not contain backslashes")
}
/**
* Tests that trailing backslashes are handled correctly on Windows.
* This test runs ONLY on Windows agents in TeamCity.
*/
@Test
@EnabledOnOs(OS.WINDOWS)
fun `normalizeDirectoryPath handles Windows trailing separators`() {
val pathWithTrailing = "C:\\Users\\test\\"
val normalized = WelcomeScreenNewFileDialog.normalizeDirectoryPath(pathWithTrailing)
assertEquals("C:/Users/test", normalized)
}
}