diff --git a/Extension/src/LanguageServer/configurations.ts b/Extension/src/LanguageServer/configurations.ts index b4686505c..dc52a6cdd 100644 --- a/Extension/src/LanguageServer/configurations.ts +++ b/Extension/src/LanguageServer/configurations.ts @@ -14,6 +14,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import * as which from 'which'; import { logAndReturn, returns } from '../Utility/Async/returns'; +import { escapePathForSquiggles } from '../Utility/Text/escape'; import * as util from '../common'; import { isWindows } from '../constants'; import { getOutputChannelLogger } from '../logger'; @@ -2119,10 +2120,8 @@ export class CppProperties { continue; } - // Escape the path string for literal use in a regular expression - // Need to escape any quotes to match the original text - let escapedPath: string = curPath.replace(/"/g, '\\"'); - escapedPath = escapedPath.replace(/[-\"\/\\^$*+?.()|[\]{}]/g, '\\$&'); + // Escape the parsed path for a literal regex match against its JSON spelling. + const escapedPath: string = escapePathForSquiggles(curPath); // Create a pattern to search for the path with either a quote or semicolon immediately before and after, // and extend that pattern to the next quote before and next quote after it. diff --git a/Extension/src/Utility/Text/escape.ts b/Extension/src/Utility/Text/escape.ts new file mode 100644 index 000000000..d4e815ec8 --- /dev/null +++ b/Extension/src/Utility/Text/escape.ts @@ -0,0 +1,9 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +export function escapePathForSquiggles(s: string): string { + return s.replace(/[-"\/\\^$*+?.()|[\]{}]/g, (character: string): string => + character === '"' ? '\\\\"' : `\\${character}`); +} diff --git a/Extension/test/unit/escape.test.ts b/Extension/test/unit/escape.test.ts new file mode 100644 index 000000000..716eab97a --- /dev/null +++ b/Extension/test/unit/escape.test.ts @@ -0,0 +1,32 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import { describe, it } from 'mocha'; +import { doesNotMatch, match, strictEqual } from 'node:assert'; +import { escapePathForSquiggles } from '../../src/Utility/Text/escape'; + +describe('Text escaping', () => { + it('escapes paths for matching their JSON spelling', () => { + strictEqual(escapePathForSquiggles('"'), '\\\\"'); + strictEqual(escapePathForSquiggles('\\'), '\\\\'); + strictEqual(escapePathForSquiggles('.*'), '\\.\\*'); + + const parsedPath: string = String.raw`C:\src"quoted"`; + const sourcePath: string = String.raw`C:\src\"quoted\"`; + const pattern: RegExp = new RegExp(`^${escapePathForSquiggles(parsedPath)}$`); + match(sourcePath, pattern); + doesNotMatch(parsedPath, pattern); + }); + + it('handles repeated backslashes and regex metacharacters', () => { + const parsedPath: string = String.raw`C:\\sdk\\[headers]+(x)?.h\\say"hello"and"goodbye`; + const sourcePath: string = String.raw`C:\\sdk\\[headers]+(x)?.h\\say\"hello\"and\"goodbye`; + const pattern: RegExp = new RegExp(`^${escapePathForSquiggles(parsedPath)}$`); + + match(sourcePath, pattern); + doesNotMatch(String.raw`C:\sdk\[headers]+(x)?.h\say\"hello\"and\"goodbye`, pattern); + doesNotMatch(String.raw`C:\\sdk\\headers+(x)?.h\\say\"hello\"and\"goodbye`, pattern); + }); +});