fix(core): preserve backslashes before newlines in unescaping logic

Fixes #18469 by updating the unescapeStringForGeminiBug regex to exclude literal newlines. This ensures C-macros with backslash-newline continuations are not corrupted during file processing.
This commit is contained in:
Coco Sheng
2026-05-13 13:43:07 -04:00
parent 749657cbf9
commit fa79eca7d1
3 changed files with 42 additions and 36 deletions
@@ -47,10 +47,11 @@ describe('editCorrector', () => {
'\nCorrect\t`',
);
});
it('should handle backslash followed by actual newline character', () => {
expect(unescapeStringForGeminiBug('\\\n')).toBe('\n');
it('should NOT strip backslash before actual newline character', () => {
// FIX FOR ISSUE #18469: This test now expects the backslash to be PRESERVED.
expect(unescapeStringForGeminiBug('\\\n')).toBe('\\\n');
expect(unescapeStringForGeminiBug('First line\\\nSecond line')).toBe(
'First line\nSecond line',
'First line\\\nSecond line',
);
});
it('should handle multiple backslashes before an escapable character (aggressive unescaping)', () => {
@@ -75,7 +76,7 @@ describe('editCorrector', () => {
it('should handle complex cases with mixed slashes and characters', () => {
expect(
unescapeStringForGeminiBug('\\\\\\\nLine1\\\nLine2\\tTab\\\\`Tick\\"'),
).toBe('\nLine1\nLine2\tTab`Tick"');
).toBe('\\\nLine1\\\nLine2\tTab`Tick"');
});
it('should handle escaped backslashes', () => {
expect(unescapeStringForGeminiBug('\\\\')).toBe('\\');
+25 -32
View File
@@ -139,44 +139,37 @@ Return ONLY the corrected string in the specified JSON format with the key 'corr
export function unescapeStringForGeminiBug(inputString: string): string {
// Regex explanation:
// \\ : Matches exactly one literal backslash character.
// (n|t|r|'|"|`|\\|\n) : This is a capturing group. It matches one of the following:
// (n|t|r|'|"|`|\\) : This is a capturing group. It matches one of the following:
// n, t, r, ', ", ` : These match the literal characters 'n', 't', 'r', single quote, double quote, or backtick.
// This handles cases like "\\n", "\\`", etc.
// \\ : This matches a literal backslash. This handles cases like "\\\\" (escaped backslash).
// \n : This matches an actual newline character. This handles cases where the input
// string might have something like "\\\n" (a literal backslash followed by a newline).
// g : Global flag, to replace all occurrences.
return inputString.replace(
/\\+(n|t|r|'|"|`|\\|\n)/g,
(match, capturedChar) => {
// 'match' is the entire erroneous sequence, e.g., if the input (in memory) was "\\\\`", match is "\\\\`".
// 'capturedChar' is the character that determines the true meaning, e.g., '`'.
return inputString.replace(/\\+(n|t|r|'|"|`|\\)/g, (match, capturedChar) => {
// 'match' is the entire erroneous sequence, e.g., if the input (in memory) was "\\\\`", match is "\\\\`".
// 'capturedChar' is the character that determines the true meaning, e.g., '`'.
switch (capturedChar) {
case 'n':
return '\n'; // Correctly escaped: \n (newline character)
case 't':
return '\t'; // Correctly escaped: \t (tab character)
case 'r':
return '\r'; // Correctly escaped: \r (carriage return character)
case "'":
return "'"; // Correctly escaped: ' (apostrophe character)
case '"':
return '"'; // Correctly escaped: " (quotation mark character)
case '`':
return '`'; // Correctly escaped: ` (backtick character)
case '\\': // This handles when 'capturedChar' is a literal backslash
return '\\'; // Replace escaped backslash (e.g., "\\\\") with single backslash
case '\n': // This handles when 'capturedChar' is an actual newline
return '\n'; // Replace the whole erroneous sequence (e.g., "\\\n" in memory) with a clean newline
default:
// This fallback should ideally not be reached if the regex captures correctly.
// It would return the original matched sequence if an unexpected character was captured.
return match;
}
},
);
switch (capturedChar) {
case 'n':
return '\n'; // Correctly escaped: \n (newline character)
case 't':
return '\t'; // Correctly escaped: \t (tab character)
case 'r':
return '\r'; // Correctly escaped: \r (carriage return character)
case "'":
return "'"; // Correctly escaped: ' (apostrophe character)
case '"':
return '"'; // Correctly escaped: " (quotation mark character)
case '`':
return '`'; // Correctly escaped: ` (backtick character)
case '\\': // This handles when 'capturedChar' is a literal backslash
return '\\'; // Replace escaped backslash (e.g., "\\\\") with single backslash
default:
// This fallback should ideally not be reached if the regex captures correctly.
// It would return the original matched sequence if an unexpected character was captured.
return match;
}
});
}
export function resetEditCorrectorCaches_TEST_ONLY() {
+12
View File
@@ -809,6 +809,18 @@ describe('fileUtils', () => {
expect(result.error).toBeUndefined();
});
it('should preserve backslashes in C-style macros', async () => {
const content =
'#define MY_MACRO \\\n do { \\\n something(); \\\n } while (0)';
actualNodeFs.writeFileSync(testTextFilePath, content);
const result = await processSingleFileContent(
testTextFilePath,
tempRootDir,
new StandardFileSystemService(),
);
expect(result.llmContent).toBe(content);
});
it('should handle file not found', async () => {
const result = await processSingleFileContent(
nonexistentFilePath,