From b0070af9d1dfebbf80a2e8e2876b9bd4ec5e3aa4 Mon Sep 17 00:00:00 2001 From: leoca Date: Thu, 29 Oct 2026 18:58:18 +0000 Subject: [PATCH] fix(hooks): return new quote state from toggleQuote instead of mutating it AGENTS.md asks for new objects rather than mutation. toggleQuote now returns the updated quote state (or null when the character is not a quote it toggles), skipOuterSpan returns it alongside the skipped index, and each scanner reassigns its own local state. --- scripts/lib/shell-substitution.js | 70 ++++++++++++++++++------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/scripts/lib/shell-substitution.js b/scripts/lib/shell-substitution.js index 5b747f0d0..efe6a9be1 100644 --- a/scripts/lib/shell-substitution.js +++ b/scripts/lib/shell-substitution.js @@ -120,23 +120,21 @@ function escapesActive(quotes) { } /** - * Toggle quote state for the quote character at `source[i]`. - * Returns true when it opened or closed a quote. Callers consume escapes + * Quote state after the quote character at `source[i]`, as a new object, or + * null when it neither opens nor closes a quote. Callers consume escapes * first, so a quote after a backslash here still closes (`'C:\'`, `"C:\\"`), * while `\'` inside `$'...'` is consumed before it can close the string. */ function toggleQuote(source, i, quotes) { const ch = source[i]; if (ch === "'" && !quotes.inDouble) { - quotes.inSingle = !quotes.inSingle; - quotes.ansiC = quotes.inSingle && opensAnsiC(source, i); - return true; + const inSingle = !quotes.inSingle; + return { ...quotes, inSingle, ansiC: inSingle && opensAnsiC(source, i) }; } if (ch === '"' && !quotes.inSingle) { - quotes.inDouble = !quotes.inDouble; - return true; + return { ...quotes, inDouble: !quotes.inDouble }; } - return false; + return null; } /** @@ -144,7 +142,7 @@ function toggleQuote(source, i, quotes) { * is at `start`, honoring quotes and escapes inside the span. */ function findParenSpanEnd(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let i = start; while (i < source.length && depth > 0) { @@ -153,7 +151,10 @@ function findParenSpanEnd(source, start) { i += 2; continue; } - if (!toggleQuote(source, i, quotes) && !quotes.inSingle && !quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) { + quotes = toggled; + } else if (!quotes.inSingle && !quotes.inDouble) { depth += parenDelta(inner); } i += 1; @@ -179,17 +180,19 @@ function findBacktickEnd(source, start) { /** * Top-level scan step shared by the group extractors: escapes, quote state, - * `$(...)` and backtick spans. Returns the last index consumed so a `for` - * loop's `i++` resumes after it, or -1 when the caller must inspect `source[i]`. + * `$(...)` and backtick spans. Returns the quote state after `source[i]` and + * `skipped`, the last index consumed so a `for` loop's `i++` resumes after it, + * or -1 when the caller must inspect `source[i]`. */ function skipOuterSpan(source, i, quotes) { const ch = source[i]; - if (ch === '\\' && escapesActive(quotes)) return i + 1; - if (toggleQuote(source, i, quotes)) return i; - if (quotes.inSingle || quotes.inDouble) return i; - if (ch === '$' && source[i + 1] === '(') return findParenSpanEnd(source, i + 2) - 1; - if (ch === '`') return findBacktickEnd(source, i + 1); - return -1; + if (ch === '\\' && escapesActive(quotes)) return { skipped: i + 1, quotes }; + const toggled = toggleQuote(source, i, quotes); + if (toggled) return { skipped: i, quotes: toggled }; + if (quotes.inSingle || quotes.inDouble) return { skipped: i, quotes }; + if (ch === '$' && source[i + 1] === '(') return { skipped: findParenSpanEnd(source, i + 2) - 1, quotes }; + if (ch === '`') return { skipped: findBacktickEnd(source, i + 1), quotes }; + return { skipped: -1, quotes }; } /** @@ -197,7 +200,7 @@ function skipOuterSpan(source, i, quotes) { * the body text and the index of the closing paren (or where input ran out). */ function readSubshellBody(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let body = ''; let i = start; @@ -210,7 +213,10 @@ function readSubshellBody(source, start) { i = Math.min(i + 2, source.length); continue; } - if (!toggleQuote(source, i, quotes) && !quotes.inSingle && !quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) { + quotes = toggled; + } else if (!quotes.inSingle && !quotes.inDouble) { depth += parenDelta(inner); if (depth === 0) break; } @@ -240,12 +246,13 @@ function readSubshellBody(source, start) { function extractSubshellGroups(input) { const source = String(input || ''); const groups = []; - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; for (let i = 0; i < source.length; i++) { - const skipped = skipOuterSpan(source, i, quotes); - if (skipped !== -1) { - i = skipped; + const step = skipOuterSpan(source, i, quotes); + quotes = step.quotes; + if (step.skipped !== -1) { + i = step.skipped; continue; } if (source[i] !== '(') continue; @@ -285,7 +292,7 @@ function opensBraceGroup(source, i) { * the body text and the index of the closing brace (or where input ran out). */ function readBraceBody(source, start) { - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; let depth = 1; let body = ''; let i = start; @@ -297,7 +304,9 @@ function readBraceBody(source, start) { i = Math.min(i + 2, source.length); continue; } - if (toggleQuote(source, i, quotes) || quotes.inSingle || quotes.inDouble) { + const toggled = toggleQuote(source, i, quotes); + if (toggled) quotes = toggled; + if (toggled || quotes.inSingle || quotes.inDouble) { body += inner; i += 1; continue; @@ -347,12 +356,13 @@ function readBraceBody(source, start) { function extractBraceGroups(input) { const source = String(input || ''); const groups = []; - const quotes = { inSingle: false, inDouble: false, ansiC: false }; + let quotes = { inSingle: false, inDouble: false, ansiC: false }; for (let i = 0; i < source.length; i++) { - const skipped = skipOuterSpan(source, i, quotes); - if (skipped !== -1) { - i = skipped; + const step = skipOuterSpan(source, i, quotes); + quotes = step.quotes; + if (step.skipped !== -1) { + i = step.skipped; continue; } if (source[i] === '(') {