fix: restrict @-mention parsing to line-start or whitespace boundaries (#7876)

Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
roomote[bot] 2025-09-11 19:37:10 -04:00 committed by GitHub
parent 9c43282c58
commit 03709fd9a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 88 additions and 18 deletions

View file

@ -3,37 +3,55 @@ import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
describe("mentionRegex and mentionRegexGlobal", () => {
// Test cases for various mention types
const testCases = [
// Basic file paths
// Basic file paths at line start
{ input: "@/path/to/file.txt", expected: ["@/path/to/file.txt"] },
{ input: "@/file.js", expected: ["@/file.js"] },
{ input: "@/folder/", expected: ["@/folder/"] },
// File paths with escaped spaces
// File paths with escaped spaces at line start
{ input: "@/path/to/file\\ with\\ spaces.txt", expected: ["@/path/to/file\\ with\\ spaces.txt"] },
{ input: "@/users/my\\ project/report\\ final.pdf", expected: ["@/users/my\\ project/report\\ final.pdf"] },
{ input: "@/folder\\ with\\ spaces/", expected: ["@/folder\\ with\\ spaces/"] },
{ input: "@/a\\ b\\ c.txt", expected: ["@/a\\ b\\ c.txt"] },
// URLs
// URLs at line start
{ input: "@http://example.com", expected: ["@http://example.com"] },
{ input: "@https://example.com/path?query=1", expected: ["@https://example.com/path?query=1"] },
// Other mentions
// Other mentions at line start
{ input: "@problems", expected: ["@problems"] },
{ input: "@git-changes", expected: ["@git-changes"] },
{ input: "@terminal", expected: ["@terminal"] },
{ input: "@a1b2c3d", expected: ["@a1b2c3d"] }, // Git commit hash (short)
{ input: "@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", expected: ["@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"] }, // Git commit hash (long)
// Mentions within text
// Mentions after whitespace (valid)
{
input: "Check file @/path/to/file\\ with\\ spaces.txt for details.",
expected: ["@/path/to/file\\ with\\ spaces.txt"],
},
{ input: "See @problems and @terminal output.", expected: ["@problems", "@terminal"] },
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // Trailing punctuation
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // After colon and space
{ input: "Commit @a1b2c3d, then check @/file.txt", expected: ["@a1b2c3d", "@/file.txt"] },
// NEW: Test cases for mentions mid-line without whitespace (should NOT match)
{ input: "error@https://example.com/path", expected: null }, // @ mid-word before URL
{ input: "log:@/var/log/system.log", expected: null }, // @ after colon without space
{ input: "email@problems", expected: null }, // @ mid-word before "problems"
{ input: "path=@/usr/local/bin", expected: null }, // @ after equals without space
{ input: "Error at line 42@terminal output", expected: null }, // @ mid-line before "terminal"
{ input: "commit@a1b2c3d", expected: null }, // @ mid-word before git hash
// NEW: Test cases for pasted logs (should NOT match)
{ input: "Failed to fetch@https://api.example.com/endpoint", expected: null },
{ input: "Error loading resource@/assets/image.png", expected: null },
{ input: "Stack trace:@/home/user/project/file.js:42", expected: null },
// NEW: Valid mentions with various whitespace
{ input: "Check\t@/path/to/file.txt", expected: ["@/path/to/file.txt"] }, // Tab before @
{ input: "Multiple @problems here", expected: ["@problems"] }, // Multiple spaces
{ input: "Newline\n@terminal output", expected: ["@terminal"] }, // After newline
// Negative cases (should not match or match partially)
{ input: "@/path/with unescaped space.txt", expected: ["@/path/with"] }, // Unescaped space
{ input: "@ /path/leading-space.txt", expected: null }, // Space after @
@ -41,14 +59,15 @@ describe("mentionRegex and mentionRegexGlobal", () => {
{ input: "mention@", expected: null }, // Trailing @
{ input: "@/path/trailing\\", expected: null }, // Trailing backslash (invalid escape)
{ input: "@/path/to/file\\not-a-space", expected: null }, // Backslash not followed by space
// Escaped mentions (should not match due to negative lookbehind)
{ input: "This is not a mention: \\@/path/to/file.txt", expected: null },
{ input: "Escaped \\@problems word", expected: null },
{ input: "Text with \\@https://example.com", expected: null },
{ input: "Another \\@a1b2c3d hash", expected: null },
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // Ensure non-escaped still works nearby
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash, currently incorrectly fails to match
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces within the path part
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // After space, should work
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces
]
testCases.forEach(({ input, expected }) => {
it(`should handle input: "${input}"`, () => {
@ -83,4 +102,48 @@ describe("mentionRegex and mentionRegexGlobal", () => {
expect(matches[0][1]).toBe("/path/to/escaped\\ file.txt") // Group 1 should not include '@'
expect(matches[1][1]).toBe("problems")
})
// NEW: Additional tests for the boundary restriction
describe("boundary restrictions", () => {
it("should match mentions at the start of a line", () => {
const input = "@/path/to/file.txt"
const match = input.match(mentionRegex)
expect(match).not.toBeNull()
expect(match?.[0]).toBe("@/path/to/file.txt")
})
it("should match mentions after whitespace", () => {
const input = "Check @/path/to/file.txt"
const match = input.match(mentionRegex)
expect(match).not.toBeNull()
expect(match?.[0]).toBe("@/path/to/file.txt")
})
it("should NOT match mentions mid-word or after non-whitespace", () => {
const testCases = ["error@https://example.com", "path:@/var/log", "email@problems", "42@terminal"]
testCases.forEach((input) => {
const match = input.match(mentionRegex)
expect(match).toBeNull()
})
})
it("should handle multiline text correctly", () => {
const input = `First line
@/path/on/newline.txt
Mid-line@/should/not/match.txt
After space @/should/match.txt`
const matches = Array.from(input.matchAll(mentionRegexGlobal))
expect(matches.length).toBe(2)
expect(matches[0][0]).toBe("@/path/on/newline.txt")
expect(matches[1][0]).toBe("@/should/match.txt")
})
it("should not match mentions in pasted log entries", () => {
const logEntry = "Error: Failed to load resource@https://api.example.com/data status:404"
const matches = Array.from(logEntry.matchAll(mentionRegexGlobal))
expect(matches.length).toBe(0)
})
})
})

View file

@ -1,19 +1,25 @@
/*
Mention regex:
- **Purpose**:
- To identify and highlight specific mentions in text that start with '@'.
- **Purpose**:
- To identify and highlight specific mentions in text that start with '@'.
- These mentions can be file paths, URLs, or the exact word 'problems'.
- Ensures that trailing punctuation marks (like commas, periods, etc.) are not included in the match, allowing punctuation to follow the mention without being part of it.
- Restricts @ parsing to line-start or after whitespace to avoid accidental loading from pasted logs.
- **Regex Breakdown**:
- `/@`:
- `(?:^|\s)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them.
- **Line Start or Whitespace (`^|\s`)**: The @ must be at the start of a line or preceded by whitespace.
- `(?<!\\)@`:
- **Negative Lookbehind (`(?<!\\)`)**: Ensures the @ is not escaped with a backslash.
- **@**: The mention must start with the '@' symbol.
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b)`:
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
- `(?:\/|\w+:\/\/)`:
- `(?:\/|\w+:\/\/)`:
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
- `\/`:
- `\/`:
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
- `|`: Logical OR.
- `\w+:\/\/`:
@ -25,7 +31,7 @@ Mention regex:
- **Escaped Space (`\\ `)**: Matches a backslash followed by a space (an escaped space).
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
- `|`: Logical OR.
- `problems\b`:
- `problems\b`:
- **Exact Word ('problems')**: Matches the exact word 'problems'.
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
- `|`: Logical OR.
@ -34,9 +40,9 @@ Mention regex:
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
- `[.,;:!?]?`:
- `[.,;:!?]?`:
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
- `(?=[\s\r\n]|$)`:
- `(?=[\s\r\n]|$)`:
- **Nested Positive Lookahead (`(?=[\s\r\n]|$)`)**: Ensures that the punctuation (if present) is followed by a whitespace character, a line break, or the end of the string.
- **Summary**:
@ -48,13 +54,14 @@ Mention regex:
- The exact word 'git-changes'.
- The exact word 'terminal'.
- It ensures that any trailing punctuation marks (such as ',', '.', '!', etc.) are not included in the matched mention, allowing the punctuation to follow the mention naturally in the text.
- **NEW**: The @ symbol must be at the start of a line or preceded by whitespace to prevent accidental matches in pasted logs.
- **Global Regex**:
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
*/
export const mentionRegex =
/(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
/(?:^|(?<=\s))(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
// Regex to match command mentions like /command-name anywhere in text