Skip to content

Commit 03709fd

Browse files
fix: restrict @-mention parsing to line-start or whitespace boundaries (#7876)
Co-authored-by: Roo Code <[email protected]>
1 parent 9c43282 commit 03709fd

File tree

2 files changed

+88
-18
lines changed

2 files changed

+88
-18
lines changed

src/shared/__tests__/context-mentions.spec.ts

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,71 @@ import { mentionRegex, mentionRegexGlobal } from "../context-mentions"
33
describe("mentionRegex and mentionRegexGlobal", () => {
44
// Test cases for various mention types
55
const testCases = [
6-
// Basic file paths
6+
// Basic file paths at line start
77
{ input: "@/path/to/file.txt", expected: ["@/path/to/file.txt"] },
88
{ input: "@/file.js", expected: ["@/file.js"] },
99
{ input: "@/folder/", expected: ["@/folder/"] },
1010

11-
// File paths with escaped spaces
11+
// File paths with escaped spaces at line start
1212
{ input: "@/path/to/file\\ with\\ spaces.txt", expected: ["@/path/to/file\\ with\\ spaces.txt"] },
1313
{ input: "@/users/my\\ project/report\\ final.pdf", expected: ["@/users/my\\ project/report\\ final.pdf"] },
1414
{ input: "@/folder\\ with\\ spaces/", expected: ["@/folder\\ with\\ spaces/"] },
1515
{ input: "@/a\\ b\\ c.txt", expected: ["@/a\\ b\\ c.txt"] },
1616

17-
// URLs
17+
// URLs at line start
1818
{ input: "@http://example.com", expected: ["@http://example.com"] },
1919
{ input: "@https://example.com/path?query=1", expected: ["@https://example.com/path?query=1"] },
2020

21-
// Other mentions
21+
// Other mentions at line start
2222
{ input: "@problems", expected: ["@problems"] },
2323
{ input: "@git-changes", expected: ["@git-changes"] },
2424
{ input: "@terminal", expected: ["@terminal"] },
2525
{ input: "@a1b2c3d", expected: ["@a1b2c3d"] }, // Git commit hash (short)
2626
{ input: "@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", expected: ["@a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0"] }, // Git commit hash (long)
2727

28-
// Mentions within text
28+
// Mentions after whitespace (valid)
2929
{
3030
input: "Check file @/path/to/file\\ with\\ spaces.txt for details.",
3131
expected: ["@/path/to/file\\ with\\ spaces.txt"],
3232
},
3333
{ input: "See @problems and @terminal output.", expected: ["@problems", "@terminal"] },
34-
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // Trailing punctuation
34+
{ input: "URL: @https://example.com.", expected: ["@https://example.com"] }, // After colon and space
3535
{ input: "Commit @a1b2c3d, then check @/file.txt", expected: ["@a1b2c3d", "@/file.txt"] },
3636

37+
// NEW: Test cases for mentions mid-line without whitespace (should NOT match)
38+
{ input: "error@https://example.com/path", expected: null }, // @ mid-word before URL
39+
{ input: "log:@/var/log/system.log", expected: null }, // @ after colon without space
40+
{ input: "email@problems", expected: null }, // @ mid-word before "problems"
41+
{ input: "path=@/usr/local/bin", expected: null }, // @ after equals without space
42+
{ input: "Error at line 42@terminal output", expected: null }, // @ mid-line before "terminal"
43+
{ input: "commit@a1b2c3d", expected: null }, // @ mid-word before git hash
44+
45+
// NEW: Test cases for pasted logs (should NOT match)
46+
{ input: "Failed to fetch@https://api.example.com/endpoint", expected: null },
47+
{ input: "Error loading resource@/assets/image.png", expected: null },
48+
{ input: "Stack trace:@/home/user/project/file.js:42", expected: null },
49+
50+
// NEW: Valid mentions with various whitespace
51+
{ input: "Check\t@/path/to/file.txt", expected: ["@/path/to/file.txt"] }, // Tab before @
52+
{ input: "Multiple @problems here", expected: ["@problems"] }, // Multiple spaces
53+
{ input: "Newline\n@terminal output", expected: ["@terminal"] }, // After newline
54+
3755
// Negative cases (should not match or match partially)
3856
{ input: "@/path/with unescaped space.txt", expected: ["@/path/with"] }, // Unescaped space
3957
{ input: "@ /path/leading-space.txt", expected: null }, // Space after @
4058
{ input: "[email protected]", expected: null }, // Email address
4159
{ input: "mention@", expected: null }, // Trailing @
4260
{ input: "@/path/trailing\\", expected: null }, // Trailing backslash (invalid escape)
4361
{ input: "@/path/to/file\\not-a-space", expected: null }, // Backslash not followed by space
62+
4463
// Escaped mentions (should not match due to negative lookbehind)
4564
{ input: "This is not a mention: \\@/path/to/file.txt", expected: null },
4665
{ input: "Escaped \\@problems word", expected: null },
4766
{ input: "Text with \\@https://example.com", expected: null },
4867
{ input: "Another \\@a1b2c3d hash", expected: null },
49-
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // Ensure non-escaped still works nearby
50-
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash, currently incorrectly fails to match
51-
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces within the path part
68+
{ input: "Not escaped @terminal", expected: ["@terminal"] }, // After space, should work
69+
{ input: "Double escape \\\\@/should/match", expected: null }, // Double backslash escapes the backslash
70+
{ input: "Text with \\@/escaped/path\\ with\\ spaces.txt", expected: null }, // Escaped mention with escaped spaces
5271
]
5372
testCases.forEach(({ input, expected }) => {
5473
it(`should handle input: "${input}"`, () => {
@@ -83,4 +102,48 @@ describe("mentionRegex and mentionRegexGlobal", () => {
83102
expect(matches[0][1]).toBe("/path/to/escaped\\ file.txt") // Group 1 should not include '@'
84103
expect(matches[1][1]).toBe("problems")
85104
})
105+
106+
// NEW: Additional tests for the boundary restriction
107+
describe("boundary restrictions", () => {
108+
it("should match mentions at the start of a line", () => {
109+
const input = "@/path/to/file.txt"
110+
const match = input.match(mentionRegex)
111+
expect(match).not.toBeNull()
112+
expect(match?.[0]).toBe("@/path/to/file.txt")
113+
})
114+
115+
it("should match mentions after whitespace", () => {
116+
const input = "Check @/path/to/file.txt"
117+
const match = input.match(mentionRegex)
118+
expect(match).not.toBeNull()
119+
expect(match?.[0]).toBe("@/path/to/file.txt")
120+
})
121+
122+
it("should NOT match mentions mid-word or after non-whitespace", () => {
123+
const testCases = ["error@https://example.com", "path:@/var/log", "email@problems", "42@terminal"]
124+
125+
testCases.forEach((input) => {
126+
const match = input.match(mentionRegex)
127+
expect(match).toBeNull()
128+
})
129+
})
130+
131+
it("should handle multiline text correctly", () => {
132+
const input = `First line
133+
@/path/on/newline.txt
134+
Mid-line@/should/not/match.txt
135+
After space @/should/match.txt`
136+
137+
const matches = Array.from(input.matchAll(mentionRegexGlobal))
138+
expect(matches.length).toBe(2)
139+
expect(matches[0][0]).toBe("@/path/on/newline.txt")
140+
expect(matches[1][0]).toBe("@/should/match.txt")
141+
})
142+
143+
it("should not match mentions in pasted log entries", () => {
144+
const logEntry = "Error: Failed to load resource@https://api.example.com/data status:404"
145+
const matches = Array.from(logEntry.matchAll(mentionRegexGlobal))
146+
expect(matches.length).toBe(0)
147+
})
148+
})
86149
})

src/shared/context-mentions.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
/*
22
Mention regex:
3-
- **Purpose**:
4-
- To identify and highlight specific mentions in text that start with '@'.
3+
- **Purpose**:
4+
- To identify and highlight specific mentions in text that start with '@'.
55
- These mentions can be file paths, URLs, or the exact word 'problems'.
66
- 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.
7+
- Restricts @ parsing to line-start or after whitespace to avoid accidental loading from pasted logs.
78
89
- **Regex Breakdown**:
9-
- `/@`:
10+
- `(?:^|\s)`:
11+
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them.
12+
- **Line Start or Whitespace (`^|\s`)**: The @ must be at the start of a line or preceded by whitespace.
13+
14+
- `(?<!\\)@`:
15+
- **Negative Lookbehind (`(?<!\\)`)**: Ensures the @ is not escaped with a backslash.
1016
- **@**: The mention must start with the '@' symbol.
1117
1218
- `((?:\/|\w+:\/\/)[^\s]+?|problems\b|git-changes\b)`:
1319
- **Capturing Group (`(...)`)**: Captures the part of the string that matches one of the specified patterns.
14-
- `(?:\/|\w+:\/\/)`:
20+
- `(?:\/|\w+:\/\/)`:
1521
- **Non-Capturing Group (`(?:...)`)**: Groups the alternatives without capturing them for back-referencing.
16-
- `\/`:
22+
- `\/`:
1723
- **Slash (`/`)**: Indicates that the mention is a file or folder path starting with a '/'.
1824
- `|`: Logical OR.
1925
- `\w+:\/\/`:
@@ -25,7 +31,7 @@ Mention regex:
2531
- **Escaped Space (`\\ `)**: Matches a backslash followed by a space (an escaped space).
2632
- **Non-Greedy (`+?`)**: Ensures the smallest possible match, preventing the inclusion of trailing punctuation.
2733
- `|`: Logical OR.
28-
- `problems\b`:
34+
- `problems\b`:
2935
- **Exact Word ('problems')**: Matches the exact word 'problems'.
3036
- **Word Boundary (`\b`)**: Ensures that 'problems' is matched as a whole word and not as part of another word (e.g., 'problematic').
3137
- `|`: Logical OR.
@@ -34,9 +40,9 @@ Mention regex:
3440
- **Word Boundary (`\b`)**: Ensures that 'terminal' is matched as a whole word and not as part of another word (e.g., 'terminals').
3541
- `(?=[.,;:!?]?(?=[\s\r\n]|$))`:
3642
- **Positive Lookahead (`(?=...)`)**: Ensures that the match is followed by specific patterns without including them in the match.
37-
- `[.,;:!?]?`:
43+
- `[.,;:!?]?`:
3844
- **Optional Punctuation (`[.,;:!?]?`)**: Matches zero or one of the specified punctuation marks.
39-
- `(?=[\s\r\n]|$)`:
45+
- `(?=[\s\r\n]|$)`:
4046
- **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.
4147
4248
- **Summary**:
@@ -48,13 +54,14 @@ Mention regex:
4854
- The exact word 'git-changes'.
4955
- The exact word 'terminal'.
5056
- 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.
57+
- **NEW**: The @ symbol must be at the start of a line or preceded by whitespace to prevent accidental matches in pasted logs.
5158
5259
- **Global Regex**:
5360
- `mentionRegexGlobal`: Creates a global version of the `mentionRegex` to find all matches within a given string.
5461
5562
*/
5663
export const mentionRegex =
57-
/(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
64+
/(?:^|(?<=\s))(?<!\\)@((?:\/|\w+:\/\/)(?:[^\s\\]|\\ )+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/
5865
export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g")
5966

6067
// Regex to match command mentions like /command-name anywhere in text

0 commit comments

Comments
 (0)