Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
36 changes: 31 additions & 5 deletions src/configurationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,25 +490,35 @@ export class JavaDebugConfigurationProvider implements vscode.DebugConfiguration
const excludes: Map<string, boolean> = new Map<string, boolean>();
for (const p of paths) {
if (p.startsWith("!")) {
let exclude = p.substr(1);
let exclude = p.slice(1);
let isDirect: boolean;

if (/[\\/]$/.test(exclude)) {
Comment thread
tthornton3-chwy marked this conversation as resolved.
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;
}

result.push(vscode.Uri.file(p).fsPath);
}

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;
}
}
Expand Down Expand Up @@ -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;
Expand Down
105 changes: 105 additions & 0 deletions test/configurationProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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<string[]>;

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<void> {
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", "/");
});
});
});