diff --git a/package.nls.json b/package.nls.json index 8634a297..fe49e707 100644 --- a/package.nls.json +++ b/package.nls.json @@ -8,12 +8,12 @@ "java.debugger.launch.modulePaths.auto": "Automatically resolve the module paths of current project.", "java.debugger.launch.modulePaths.runtime": "The module paths within 'runtime' scope of current project.", "java.debugger.launch.modulePaths.test": "The module paths within 'test' scope of current project.", - "java.debugger.launch.modulePaths.exclude": "The path after '!' will be excluded from the modulePaths.", + "java.debugger.launch.modulePaths.exclude": "The path after '!' will be excluded from the modulePaths. A trailing slash or backslash will treat the path as an exact match.", "java.debugger.launch.classPaths.description": "The classpaths for launching the JVM. If not specified, the debugger will automatically resolve from current project.", "java.debugger.launch.classPaths.auto": "Automatically resolve the classpaths of current project.", "java.debugger.launch.classPaths.runtime": "The classpaths within 'runtime' scope of current project.", "java.debugger.launch.classPaths.test": "The classpaths within 'test' scope of current project.", - "java.debugger.launch.classPaths.exclude": "The path after '!' will be excluded from the classpaths.", + "java.debugger.launch.classPaths.exclude": "The path after '!' will be excluded from the classpaths. A trailing slash or backslash will treat the path as an exact match.", "java.debugger.launch.sourcePaths.description": "The extra source directories of the program. The debugger looks for source code from project settings by default. This option allows the debugger to look for source code in extra directories.", "java.debugger.launch.encoding.description": "The file.encoding setting for the JVM. Possible values can be found in https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html.", "java.debugger.launch.cwd.description": "The working directory of the program. Defaults to the current workspace root.", diff --git a/src/configurationProvider.ts b/src/configurationProvider.ts index 92db8e11..b609ce5b 100644 --- a/src/configurationProvider.ts +++ b/src/configurationProvider.ts @@ -490,12 +490,22 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration const excludes: Map = new Map(); for (const p of paths) { if (p.startsWith("!")) { - let exclude = p.substr(1); + let exclude = p.slice(1); + let isDirect: boolean; + + if (/[\\/]$/.test(exclude)) { + exclude = exclude.slice(0, -1); + isDirect = true; + } else { + isDirect = this.isFilePath(exclude); + } + if (!path.isAbsolute(exclude)) { exclude = path.join(folder?.uri.fsPath || "", exclude); } + // use Uri to normalize the fs path - excludes.set(vscode.Uri.file(exclude).fsPath, this.isFilePath(exclude)); + excludes.set(vscode.Uri.file(exclude).fsPath, isDirect); continue; } @@ -503,12 +513,12 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration } return result.filter((r) => { - for (const [excludedPath, isFile] of excludes.entries()) { - if (isFile && r === excludedPath) { + for (const [excludedPath, isDirect] of excludes.entries()) { + if (isDirect && stripTrailingSeparators(r) === stripTrailingSeparators(excludedPath)) { return false; } - if (!isFile && r.startsWith(excludedPath)) { + if (!isDirect && r.startsWith(excludedPath)) { return false; } } @@ -824,6 +834,22 @@ async function updateDebugSettings(event?: vscode.ConfigurationChangeEvent) { } } +/** + * Removes trailing path separators for comparison, leaving filesystem roots + * such as "/", "\\", or "C:\\" unchanged (including Windows drive roots when + * running on POSIX). + */ +function stripTrailingSeparators(fsPath: string): string { + if (!fsPath || fsPath === "/" || fsPath === "\\" || fsPath === path.parse(fsPath).root) { + return fsPath; + } + // Windows drive root, recognized even when the host platform is POSIX. + if (/^[A-Za-z]:[\\/]$/.test(fsPath)) { + return fsPath; + } + return fsPath.replace(/[\\/]+$/, ""); +} + function needsBuildWorkspace(): boolean { const javaConfig: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration("java"); return javaConfig?.debug?.settings?.forceBuildBeforeLaunch; diff --git a/test/configurationProvider.test.ts b/test/configurationProvider.test.ts new file mode 100644 index 00000000..61d4ed37 --- /dev/null +++ b/test/configurationProvider.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as assert from "assert"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as vscode from "vscode"; + +import { JavaDebugConfigurationProvider } from "../src/configurationProvider"; + +interface TestWorkspace { + root: string; + folder: vscode.WorkspaceFolder; + libDir: string; + jarPath: string; +} + +type FilterExcluded = ( + folder: vscode.WorkspaceFolder | undefined, + paths: string[], +) => Promise; + +function createTestWorkspace(): TestWorkspace { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "java-debug-cp-test-")); + const libDir = path.join(root, "lib"); + fs.mkdirSync(libDir); + const jarPath = path.join(libDir, "foo.jar"); + fs.writeFileSync(jarPath, ""); + return { + root, + folder: { + uri: vscode.Uri.file(root), + name: "test-workspace", + index: 0, + }, + libDir, + jarPath, + }; +} + +function getFilterExcluded(provider: JavaDebugConfigurationProvider): FilterExcluded { + return (provider as unknown as { filterExcluded: FilterExcluded }).filterExcluded.bind(provider); +} + +suite("JavaDebugConfigurationProvider", () => { + const workspaces: TestWorkspace[] = []; + + suiteSetup(() => { + // configurationProvider requires ../package.json relative to out/src/ + const outPackageJson = path.join(__dirname, "../package.json"); + if (!fs.existsSync(outPackageJson)) { + fs.copyFileSync(path.join(__dirname, "../../package.json"), outPackageJson); + } + }); + + teardown(() => { + while (workspaces.length > 0) { + const workspace = workspaces.pop()!; + fs.rmSync(workspace.root, { recursive: true, force: true }); + } + }); + + suite("filterExcluded exact-match exclusions", () => { + async function assertExactDirectoryExclusion( + excludeSuffix: "\\" | "/", + label: string, + includeSuffix: "" | "\\" | "/" = "", + ): Promise { + const workspace = createTestWorkspace(); + workspaces.push(workspace); + + const libDirFs = vscode.Uri.file(workspace.libDir).fsPath; + const jarFs = vscode.Uri.file(workspace.jarPath).fsPath; + const filterExcluded = getFilterExcluded(new JavaDebugConfigurationProvider()); + const result = await filterExcluded(workspace.folder, [ + `${libDirFs}${includeSuffix}`, + jarFs, + `!${workspace.libDir}${excludeSuffix}`, + ]); + + assert.deepStrictEqual( + result, + [jarFs], + `${label}: trailing slash should exact-exclude only the directory entry, not paths beneath it`, + ); + } + + test("treats a trailing backslash as an exact match (Windows-style paths)", async () => { + await assertExactDirectoryExclusion("\\", "Windows-style"); + }); + + test("treats a trailing forward slash as an exact match (Linux-style paths)", async () => { + await assertExactDirectoryExclusion("/", "Linux-style"); + }); + + test("exact-matches when the included Windows-style path ends with a backslash", async () => { + await assertExactDirectoryExclusion("\\", "Windows-style included trailing separator", "\\"); + }); + + test("exact-matches when the included Linux-style path ends with a forward slash", async () => { + await assertExactDirectoryExclusion("/", "Linux-style included trailing separator", "/"); + }); + }); +});