Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

814 lines
24 KiB
TypeScript

import {
applySourceEdits,
CodePlaceholderCompileError,
createCodePlaceholderCompilationContext,
isOffsetInRanges,
type SourceEdit,
} from '@/lib/execution/code-placeholders/shared'
import type {
CodePlaceholderOccurrence,
CompiledCodePlaceholders,
InternalCompileCodePlaceholdersInput,
} from '@/lib/execution/code-placeholders/types'
interface HeredocDeclaration {
operatorStart: number
operatorEnd: number
delimiter: string
quoted: boolean
stripTabs: boolean
bodyStart: number
bodyEnd: number
removalEnd: number
}
type ShellQuote = 'none' | 'single' | 'double' | 'ansi'
interface ShellScanFrame {
kind: 'root' | 'command' | 'arithmetic' | 'backtick'
quote: ShellQuote
parenthesisDepth: number
literalRoot: boolean
}
interface ShellOccurrenceContext {
quote: ShellQuote
unsupported?: 'escaped sequence'
}
function lineEndAfterNewline(code: string, start: number): number {
const newline = code.indexOf('\n', start)
return newline === -1 ? code.length : newline + 1
}
function logicalLineEndAfterContinuations(code: string, start: number): number {
let end = lineEndAfterNewline(code, start)
while (end < code.length) {
let cursor = end - 2
if (code[cursor] === '\r') cursor -= 1
let backslashes = 0
while (cursor >= start && code[cursor] === '\\') {
backslashes += 1
cursor -= 1
}
if (backslashes % 2 === 0) break
end = lineEndAfterNewline(code, end)
}
return end
}
function shellCommentStarts(code: string, index: number): boolean {
if (code[index] !== '#') return false
const previous = code[index - 1]
return previous === undefined || /\s|[;&|()<>]/.test(previous)
}
function shellArithmeticCommandStarts(code: string, index: number): boolean {
if (code[index] !== '(' || code[index + 1] !== '(') return false
const previous = code[index - 1]
return previous === undefined || /\s|[;&|()<>]/.test(previous)
}
function decodeAnsiCCharacter(code: string, index: number): { value: string; end: number } {
const character = code[index]
const simple: Record<string, string> = {
a: '\x07',
b: '\b',
e: '\x1b',
E: '\x1b',
f: '\f',
n: '\n',
r: '\r',
t: '\t',
v: '\v',
'\\': '\\',
"'": "'",
'"': '"',
}
if (Object.hasOwn(simple, character)) return { value: simple[character], end: index + 1 }
const remainder = code.slice(index)
const hexadecimal = /^(?:x([0-9A-Fa-f]{1,2})|u([0-9A-Fa-f]{4})|U([0-9A-Fa-f]{8}))/.exec(remainder)
if (hexadecimal) {
const digits = hexadecimal[1] ?? hexadecimal[2] ?? hexadecimal[3]
const codePoint = Number.parseInt(digits, 16)
if (codePoint <= 0x10ffff && !(codePoint >= 0xd800 && codePoint <= 0xdfff)) {
return { value: String.fromCodePoint(codePoint), end: index + hexadecimal[0].length }
}
}
const octal = /^([0-7]{1,3})/.exec(remainder)
if (octal) {
return {
value: String.fromCodePoint(Number.parseInt(octal[1], 8)),
end: index + octal[0].length,
}
}
if (character === 'c' && code[index + 1] !== undefined) {
return {
value: String.fromCodePoint(code[index + 1].toUpperCase().codePointAt(0)! & 0x1f),
end: index + 2,
}
}
return { value: `\\${character}`, end: index + 1 }
}
function readHeredocDelimiterWord(
code: string,
start: number,
end: number
): { delimiter: string; quoted: boolean; end: number } | undefined {
let cursor = start
let delimiter = ''
let quoted = false
let consumed = false
while (cursor < end) {
const character = code[cursor]
if (/\s|[;&|()<>]/.test(character)) break
consumed = true
if (character === '\\') {
if (code[cursor + 1] === '\n') {
cursor += 2
continue
}
if (code[cursor + 1] === '\r' && code[cursor + 2] === '\n') {
cursor += 3
continue
}
quoted = true
if (cursor + 1 < end) {
delimiter += code[cursor + 1]
cursor += 2
continue
}
cursor += 1
continue
}
const dollarQuoted = character === '$' && (code[cursor + 1] === "'" || code[cursor + 1] === '"')
if (character === "'" || character === '"' || dollarQuoted) {
const quote = dollarQuoted ? code[cursor + 1] : character
const ansi = dollarQuoted && quote === "'"
quoted = true
cursor += dollarQuoted ? 2 : 1
let closed = false
while (cursor < end) {
const current = code[cursor]
if (current === quote) {
cursor += 1
closed = true
break
}
if (ansi && current === '\\' && cursor + 1 < end) {
const decoded = decodeAnsiCCharacter(code, cursor + 1)
delimiter += decoded.value
cursor = decoded.end
continue
}
if (quote === '"' && current === '\\' && cursor + 1 < end) {
const next = code[cursor + 1]
if (next === '\n') {
cursor += 2
continue
}
if (next === '\r' && code[cursor + 2] === '\n') {
cursor += 3
continue
}
if (next === '$' || next === '`' || next === '"' || next === '\\') {
delimiter += next
cursor += 2
continue
}
}
delimiter += current
cursor += 1
}
if (!closed) {
throw new CodePlaceholderCompileError('Unterminated shell heredoc delimiter', code, start)
}
continue
}
delimiter += character
cursor += 1
}
return consumed ? { delimiter, quoted, end: cursor } : undefined
}
function parseHeredocHeaders(
code: string,
lineStart: number,
lineEnd: number,
frames: ShellScanFrame[]
): Array<Omit<HeredocDeclaration, 'bodyStart' | 'bodyEnd' | 'removalEnd'>> {
const declarations: Array<Omit<HeredocDeclaration, 'bodyStart' | 'bodyEnd' | 'removalEnd'>> = []
for (let index = lineStart; index < lineEnd; index += 1) {
const frame = frames.at(-1)
if (!frame) break
const character = code[index]
if (frame.quote === 'single') {
if (character === "'") frame.quote = 'none'
continue
}
if (frame.quote === 'ansi') {
if (character === '\\') index += 1
else if (character === "'") frame.quote = 'none'
continue
}
if (frame.quote === 'double') {
if (character === '\\') index += 1
else if (character === '"') frame.quote = 'none'
else if (character === '$' && code[index + 1] === '(' && code[index + 2] === '(') {
frames.push({
kind: 'arithmetic',
quote: 'none',
parenthesisDepth: 2,
literalRoot: false,
})
index += 2
} else if (character === '$' && code[index + 1] === '(') {
frames.push({
kind: 'command',
quote: 'none',
parenthesisDepth: 1,
literalRoot: false,
})
index += 1
} else if (character === '`') {
frames.push({
kind: 'backtick',
quote: 'none',
parenthesisDepth: 0,
literalRoot: false,
})
}
continue
}
if (frame.kind === 'backtick' && character === '`') {
frames.pop()
continue
}
if (shellCommentStarts(code, index)) break
if (character === '$' && code[index + 1] === "'") {
frame.quote = 'ansi'
index += 1
continue
}
if (character === "'") {
frame.quote = 'single'
continue
}
if (character === '"') {
frame.quote = 'double'
continue
}
if (character === '\\') {
index += 1
continue
}
if (character === '$' && code[index + 1] === '(' && code[index + 2] === '(') {
frames.push({
kind: 'arithmetic',
quote: 'none',
parenthesisDepth: 2,
literalRoot: false,
})
index += 2
continue
}
if (shellArithmeticCommandStarts(code, index)) {
frames.push({
kind: 'arithmetic',
quote: 'none',
parenthesisDepth: 2,
literalRoot: false,
})
index += 1
continue
}
if (character === '$' && code[index + 1] === '(') {
frames.push({
kind: 'command',
quote: 'none',
parenthesisDepth: 1,
literalRoot: false,
})
index += 1
continue
}
if (character === '`') {
frames.push({
kind: 'backtick',
quote: 'none',
parenthesisDepth: 0,
literalRoot: false,
})
continue
}
if ((frame.kind === 'command' || frame.kind === 'arithmetic') && character === '(') {
frame.parenthesisDepth += 1
continue
}
if ((frame.kind === 'command' || frame.kind === 'arithmetic') && character === ')') {
frame.parenthesisDepth -= 1
if (frame.parenthesisDepth === 0) frames.pop()
continue
}
if (frame.kind === 'arithmetic') continue
if (character !== '<' || code[index + 1] !== '<' || code[index + 2] === '<') continue
const operatorStart = index
let cursor = index + 2
const stripTabs = code[cursor] === '-'
if (stripTabs) cursor += 1
while (cursor < lineEnd && (code[cursor] === ' ' || code[cursor] === '\t')) cursor += 1
const word = readHeredocDelimiterWord(code, cursor, lineEnd)
if (!word) continue
cursor = word.end
declarations.push({
operatorStart,
operatorEnd: cursor,
delimiter: word.delimiter,
quoted: word.quoted,
stripTabs,
})
index = cursor - 1
}
return declarations
}
function collectHeredocs(code: string): HeredocDeclaration[] {
const declarations: HeredocDeclaration[] = []
const frames: ShellScanFrame[] = [
{ kind: 'root', quote: 'none', parenthesisDepth: 0, literalRoot: false },
]
let cursor = 0
while (cursor < code.length) {
const headerEnd = logicalLineEndAfterContinuations(code, cursor)
const headers = parseHeredocHeaders(code, cursor, headerEnd, frames)
if (headers.length === 0) {
cursor = headerEnd
continue
}
let bodyCursor = headerEnd
let complete = true
for (const header of headers) {
const bodyStart = bodyCursor
let found = false
while (bodyCursor <= code.length) {
const candidateEnd = lineEndAfterNewline(code, bodyCursor)
const rawLine = code.slice(bodyCursor, candidateEnd).replace(/\n$/, '').replace(/\r$/, '')
const comparable = header.stripTabs ? rawLine.replace(/^\t+/, '') : rawLine
if (comparable === header.delimiter) {
declarations.push({
...header,
bodyStart,
bodyEnd: bodyCursor,
removalEnd: candidateEnd,
})
bodyCursor = candidateEnd
found = true
break
}
if (candidateEnd === code.length) break
bodyCursor = candidateEnd
}
if (!found) {
complete = false
break
}
}
cursor = complete ? bodyCursor : headerEnd
}
return declarations
}
function shellExpansion(name: string, quote: ShellQuote): string {
const expansion = `\${${name}}`
if (quote === 'double') return expansion
if (quote === 'single') return `'"${expansion}"'`
if (quote === 'ansi') return `'"${expansion}"$'`
return expansion
}
function isLegacyShellPlaceholder(occurrence: CodePlaceholderOccurrence): boolean {
const inner = occurrence.raw.slice(2, -2)
return inner.trim() === occurrence.name && /^[A-Za-z_][A-Za-z0-9_]*$/.test(occurrence.name)
}
function blankPreservingLines(value: string): string {
return value.replace(/[^\r\n]/g, '')
}
function stripHeredocTabs(value: string): string {
return value.replace(/(^|\n)\t+/g, '$1')
}
function preserveContinuationLines(value: string): string {
return [...value.matchAll(/\r?\n/g)].map((match) => ` \\${match[0]}`).join('')
}
function isShellAssignmentName(code: string, occurrence: CodePlaceholderOccurrence): boolean {
if (code[occurrence.end] !== '=') return false
const previous = code[occurrence.start - 1]
return previous === undefined || /\s|[;&|()]/.test(previous)
}
function getUnsupportedShellPosition(
code: string,
occurrence: CodePlaceholderOccurrence,
quote: ShellQuote
): string | undefined {
if (code[occurrence.start - 1] === '$') return 'immediately after "$"'
if (quote !== 'none') return undefined
const lineStart = Math.max(
code.lastIndexOf('\n', occurrence.start - 1),
code.lastIndexOf(';', occurrence.start - 1),
code.lastIndexOf('&', occurrence.start - 1),
code.lastIndexOf('|', occurrence.start - 1)
)
const prefix = code.slice(lineStart + 1, occurrence.start)
if (/(?:^|\s)(?:for|select|function)\s*$/.test(prefix)) return 'as a shell parser name'
if (/^\s*\(\)/.test(code.slice(occurrence.end))) return 'as a shell function name'
return undefined
}
function collectShellOccurrenceContexts(
code: string,
occurrences: CodePlaceholderOccurrence[],
start: number,
end: number,
literalRoot: boolean
): Map<CodePlaceholderOccurrence, ShellOccurrenceContext> {
const occurrenceByStart = new Map(
occurrences
.filter((occurrence) => occurrence.start >= start && occurrence.end <= end)
.map((occurrence) => [occurrence.start, occurrence] as const)
)
const contexts = new Map<CodePlaceholderOccurrence, ShellOccurrenceContext>()
const frames: ShellScanFrame[] = [
{ kind: 'root', quote: 'none', parenthesisDepth: 0, literalRoot },
]
for (let index = start; index < end; ) {
const frame = frames.at(-1)
if (!frame) break
const occurrence = occurrenceByStart.get(index)
if (occurrence) {
contexts.set(occurrence, { quote: frame.quote })
index = occurrence.end
continue
}
const character = code[index]
if (frame.quote === 'single') {
if (character === "'") frame.quote = 'none'
index += 1
continue
}
if (frame.quote === 'ansi') {
if (character === '\\') {
const escaped = occurrenceByStart.get(index + 1)
if (escaped) {
contexts.set(escaped, { quote: frame.quote, unsupported: 'escaped sequence' })
index = escaped.end
} else {
index += 2
}
} else {
if (character === "'") frame.quote = 'none'
index += 1
}
continue
}
if (frame.quote === 'double') {
if (character === '\\') {
const escaped = occurrenceByStart.get(index + 1)
if (escaped) {
contexts.set(escaped, { quote: frame.quote, unsupported: 'escaped sequence' })
index = escaped.end
} else {
index += 2
}
} else if (character === '"') {
frame.quote = 'none'
index += 1
} else if (character === '$' && code[index + 1] === '(') {
frames.push({
kind: 'command',
quote: 'none',
parenthesisDepth: 1,
literalRoot: false,
})
index += 2
} else if (character === '`') {
frames.push({
kind: 'backtick',
quote: 'none',
parenthesisDepth: 0,
literalRoot: false,
})
index += 1
} else {
index += 1
}
continue
}
if (frame.kind === 'backtick' && character === '`') {
frames.pop()
index += 1
continue
}
if (!frame.literalRoot && shellCommentStarts(code, index)) {
const newline = code.indexOf('\n', index)
index = newline === -1 || newline >= end ? end : newline + 1
continue
}
if (character === '\\') {
const escaped = occurrenceByStart.get(index + 1)
if (escaped) {
contexts.set(escaped, { quote: frame.quote, unsupported: 'escaped sequence' })
index = escaped.end
} else {
index += 2
}
continue
}
if (!frame.literalRoot && character === '$' && code[index + 1] === "'") {
frame.quote = 'ansi'
index += 2
continue
}
if (!frame.literalRoot && character === "'") {
frame.quote = 'single'
index += 1
continue
}
if (!frame.literalRoot && character === '"') {
frame.quote = 'double'
index += 1
continue
}
if (character === '$' && code[index + 1] === '(') {
frames.push({
kind: 'command',
quote: 'none',
parenthesisDepth: 1,
literalRoot: false,
})
index += 2
continue
}
if (character === '`') {
frames.push({
kind: 'backtick',
quote: 'none',
parenthesisDepth: 0,
literalRoot: false,
})
index += 1
continue
}
if (frame.kind === 'command' && character === '(') {
frame.parenthesisDepth += 1
index += 1
continue
}
if (frame.kind === 'command' && character === ')') {
frame.parenthesisDepth -= 1
if (frame.parenthesisDepth === 0) frames.pop()
index += 1
continue
}
index += 1
}
return contexts
}
export async function compileShellPlaceholders(
input: InternalCompileCodePlaceholdersInput
): Promise<CompiledCodePlaceholders> {
const context = createCodePlaceholderCompilationContext(input)
if (context.occurrences.length === 0) return context.finish(input.code)
const validateShellValue = <T extends { value: string } | undefined>(
occurrence: CodePlaceholderOccurrence,
resolved: T
): T => {
if (resolved?.value.includes('\0')) {
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" cannot contain NUL in shell code`,
input.code,
occurrence.start
)
}
return resolved
}
const resolveShellOccurrence = (occurrence: CodePlaceholderOccurrence) =>
validateShellValue(occurrence, context.resolve(occurrence))
const resolveShellValue = (occurrence: CodePlaceholderOccurrence) =>
validateShellValue(occurrence, context.resolveValue(occurrence))
const heredocs = collectHeredocs(input.code)
const shellOccurrences = context.occurrences.filter(isLegacyShellPlaceholder)
const edits: SourceEdit[] = []
const excludedRanges: Array<[number, number]> = []
for (const heredoc of heredocs) {
excludedRanges.push([heredoc.operatorStart, heredoc.operatorEnd])
excludedRanges.push([heredoc.bodyStart, heredoc.removalEnd])
const delimiterOccurrences = shellOccurrences.filter(
(occurrence) =>
occurrence.start >= heredoc.operatorStart && occurrence.end <= heredoc.operatorEnd
)
for (const occurrence of delimiterOccurrences) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported in a shell heredoc delimiter`,
input.code,
occurrence.start
)
}
}
const bodyOccurrences = shellOccurrences.filter(
(occurrence) => occurrence.start >= heredoc.bodyStart && occurrence.end <= heredoc.bodyEnd
)
if (heredoc.quoted) {
const bodyEdits: SourceEdit[] = []
let hasResolvedPlaceholder = false
for (const occurrence of bodyOccurrences) {
const resolved = resolveShellValue(occurrence)
bodyEdits.push({
start: occurrence.start - heredoc.bodyStart,
end: occurrence.end - heredoc.bodyStart,
text: resolved?.value ?? '',
})
if (resolved) hasResolvedPlaceholder = true
}
if (bodyEdits.length === 0) continue
if (!hasResolvedPlaceholder) {
edits.push(
...bodyEdits.map((edit) => ({
...edit,
start: edit.start + heredoc.bodyStart,
end: edit.end + heredoc.bodyStart,
}))
)
continue
}
let content = applySourceEdits(
input.code.slice(heredoc.bodyStart, heredoc.bodyEnd),
bodyEdits
)
if (heredoc.stripTabs) content = stripHeredocTabs(content)
const privateInput = context.createPrivateInput(content)
edits.push({
start: heredoc.operatorStart,
end: heredoc.operatorEnd,
text: `< "\${${privateInput.environmentVariable}}"${preserveContinuationLines(input.code.slice(heredoc.operatorStart, heredoc.operatorEnd))}`,
})
edits.push({
start: heredoc.bodyStart,
end: heredoc.removalEnd,
text: blankPreservingLines(input.code.slice(heredoc.bodyStart, heredoc.removalEnd)),
})
continue
}
const bodyContexts = collectShellOccurrenceContexts(
input.code,
bodyOccurrences,
heredoc.bodyStart,
heredoc.bodyEnd,
true
)
for (const occurrence of bodyOccurrences) {
const occurrenceContext = bodyContexts.get(occurrence)
if (!occurrenceContext) continue
if (occurrenceContext.unsupported) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported in an escaped shell sequence`,
input.code,
occurrence.start
)
}
continue
}
const unsupportedPosition = getUnsupportedShellPosition(
input.code,
occurrence,
occurrenceContext.quote
)
if (unsupportedPosition) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported ${unsupportedPosition}`,
input.code,
occurrence.start
)
}
continue
}
const resolved = resolveShellOccurrence(occurrence)
edits.push({
start: occurrence.start,
end: occurrence.end,
text: resolved ? shellExpansion(resolved.bindingName, occurrenceContext.quote) : '',
})
}
}
const rootOccurrences = shellOccurrences.filter(
(occurrence) => !isOffsetInRanges(occurrence.start, excludedRanges)
)
const rootContexts = collectShellOccurrenceContexts(
input.code,
rootOccurrences,
0,
input.code.length,
false
)
for (const occurrence of rootOccurrences) {
const occurrenceContext = rootContexts.get(occurrence)
if (!occurrenceContext) continue
if (occurrenceContext.unsupported) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported in an escaped shell sequence`,
input.code,
occurrence.start
)
}
continue
}
const unsupportedPosition = getUnsupportedShellPosition(
input.code,
occurrence,
occurrenceContext.quote
)
if (unsupportedPosition) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported ${unsupportedPosition}`,
input.code,
occurrence.start
)
}
continue
}
if (occurrenceContext.quote === 'none' && isShellAssignmentName(input.code, occurrence)) {
if (context.hasValue(occurrence.name)) {
if (input.analysisOnly) {
context.resolveValue(occurrence)
continue
}
throw new CodePlaceholderCompileError(
`Variable placeholder "${occurrence.name}" is not supported as a shell assignment name`,
input.code,
occurrence.start
)
}
continue
}
const resolved = resolveShellOccurrence(occurrence)
edits.push({
start: occurrence.start,
end: occurrence.end,
text: resolved ? shellExpansion(resolved.bindingName, occurrenceContext.quote) : '',
})
}
return context.finish(applySourceEdits(input.code, edits))
}