fix(parameterization): word-boundary check prevents over-eager value match

ParameterizationPreview.tokenize() matched highlight values via raw
seg.text.startsWith(value, cursor) with no word-boundary check and no
minimum length. A param value like "D" (e.g. a drive letter) lit up every
capital D in the script body — Get-ADUser, Add-Type, Disable- all rendered
as proposed-parameter pills.

Add a word-boundary guard: a candidate match is only accepted if either
side of the match either falls at start/end of the segment, OR the
adjacent character is non-alphanumeric. The guard is conditional on
whether the value itself starts/ends with a word char, so values that
begin or end in punctuation (e.g. "D:\\Folder") still match cleanly when
they sit next to whitespace or punctuation.

Surfaced 2026-05-01 while testing the suggested-fix flow with a real
PowerShell script.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-01 16:23:05 -04:00
parent 0156aae684
commit 8b0358af3b

View File

@@ -80,10 +80,27 @@ function tokenize(body: string, highlightValues: Record<string, string> | undefi
while (cursor < seg.text.length) {
let matched: { key: string; value: string } | null = null
for (const [key, value] of valueEntries) {
if (seg.text.startsWith(value, cursor)) {
matched = { key, value }
break
}
if (!seg.text.startsWith(value, cursor)) continue
// Word-boundary guard: a single-char value like "D" (drive letter)
// would otherwise light up every capital D in identifiers like
// `Get-ADUser`. We only require a boundary on a side of the value
// that itself starts/ends with a word char, so values that begin or
// end in punctuation (e.g. "D:\\Folder") still match cleanly.
const valueStartsWithWordChar = /^\w/.test(value)
const valueEndsWithWordChar = /\w$/.test(value)
const before = cursor > 0 ? seg.text[cursor - 1] : undefined
const after = cursor + value.length < seg.text.length
? seg.text[cursor + value.length]
: undefined
const startBounded = !valueStartsWithWordChar
|| before === undefined
|| !/\w/.test(before)
const endBounded = !valueEndsWithWordChar
|| after === undefined
|| !/\w/.test(after)
if (!startBounded || !endBounded) continue
matched = { key, value }
break
}
if (matched) {
flushPending()