mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-27 03:51:21 -04:00
146 lines
6.8 KiB
TypeScript
146 lines
6.8 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import { ShellScan } from "../../src/shell/scan.js"
|
|
|
|
describe("PowerShell practical syntax", () => {
|
|
test.each([
|
|
["$result = git status", ["git status"]],
|
|
["$result += (git status)", ["git status"]],
|
|
["[string]$result = git status", ["git status"]],
|
|
["$x = 1; $x++; git status", ["git status"]],
|
|
["$true; 1 + 2; 'literal'; git status", ["git status"]],
|
|
["${result}# ignored\ngit status", ["git status"]],
|
|
[
|
|
"Write-Output { ${result}# ignored\ngit status }",
|
|
["Write-Output { ${result}# ignored\ngit status }", "git status"],
|
|
],
|
|
["Switch-Branch main; Function-Name arg", ["Switch-Branch main", "Function-Name arg"]],
|
|
["(git status)", ["git status"]],
|
|
["@(git status; git diff)", ["git status", "git diff"]],
|
|
["$x = @{ status = git status; count = 1 }", ["git status"]],
|
|
["if (Test-Path file) { git status } else { git diff }", ["Test-Path file", "git status", "git diff"]],
|
|
["if ($true) { git status } elseif ($false) { git diff }", ["git status", "git diff"]],
|
|
["foreach ($file in (Get-ChildItem .)) { Get-Content $file }", ["Get-ChildItem .", "Get-Content $file"]],
|
|
["foreach ($file in Get-ChildItem .) { Get-Content $file }", ["Get-ChildItem .", "Get-Content $file"]],
|
|
["for ($i = 0; $i -lt 2; $i++) { git status }", ["git status"]],
|
|
["while (Test-Path file) { git status; break }", ["Test-Path file", "git status"]],
|
|
["do { git status } until ($true)", ["git status"]],
|
|
["function Get-Status { param($file); git status }; Get-Status", ["git status", "Get-Status"]],
|
|
["function Get-Status($file = (Get-Item .)) { git status }", ["Get-Item .", "git status"]],
|
|
[
|
|
"try { git status } catch { Write-Output $_ } finally { git diff }",
|
|
["git status", "Write-Output $_", "git diff"],
|
|
],
|
|
["& { git status }", ["& { git status }", "git status"]],
|
|
["return git status", ["git status"]],
|
|
["<# <# ignored } #> #> ignored\ngit status", ["git status"]],
|
|
["git <# ignored #> status", ["git <# ignored #> status"]],
|
|
['Write-Output "$(git status)"', ['Write-Output "$(git status)"', "git status"]],
|
|
[
|
|
'Write-Output "$(Write-Output "$(git status)")"',
|
|
['Write-Output "$(Write-Output "$(git status)")"', 'Write-Output "$(git status)"', "git status"],
|
|
],
|
|
["Write-Output @'\n$(not-a-command)\n'@; git status", ["Write-Output @'\n$(not-a-command)\n'@", "git status"]],
|
|
['Write-Output @"\n$(git status)\n"@', ['Write-Output @"\n$(git status)\n"@', "git status"]],
|
|
[
|
|
"cmd.exe --% $(literal) > literal.txt; still-literal\ngit status",
|
|
["cmd.exe --% $(literal) > literal.txt; still-literal", "git status"],
|
|
],
|
|
[
|
|
'cmd.exe --% "literal|still-literal" | Write-Output done',
|
|
['cmd.exe --% "literal|still-literal"', "Write-Output done"],
|
|
],
|
|
["Write-Output '--%' ; git status", ["Write-Output '--%'", "git status"]],
|
|
["Write-Output prefix--% literal; git status", ["Write-Output prefix--% literal", "git status"]],
|
|
["git status |\n\n# comment\nOut-String", ["git status", "Out-String"]],
|
|
] as const)("extracts command resources from %s", (source, resources) => {
|
|
const result = ShellScan.scanPowerShell(source)
|
|
expect(result.kind).toBe("scanned")
|
|
if (result.kind === "opaque") return
|
|
expect(result.commands.map((command) => command.resource)).toEqual([...resources])
|
|
})
|
|
|
|
test.each(["\n", "\r", "\r\n"])(
|
|
"distinguishes standalone continuations from escapes inside tokens: %j",
|
|
(newline) => {
|
|
expect(ShellScan.scanPowerShell(`git \`${newline}\tstatus`)).toMatchObject({
|
|
kind: "scanned",
|
|
commands: [{ words: ["git", "status"], rawWords: ["git", "status"] }],
|
|
})
|
|
expect(ShellScan.scanPowerShell(`Write-Output left\`${newline}right`)).toMatchObject({
|
|
kind: "scanned",
|
|
commands: [
|
|
{ words: ["Write-Output", `left${newline}right`], rawWords: ["Write-Output", `left\`${newline}right`] },
|
|
],
|
|
})
|
|
},
|
|
)
|
|
|
|
test("decodes literal backtick escapes without evaluating expressions", () => {
|
|
const source = '& "Wr`ite-Output" "tab`tnewline`n`u{1f642}" left`;right'
|
|
expect(ShellScan.scanPowerShell(source)).toMatchObject({
|
|
kind: "scanned",
|
|
commands: [
|
|
{
|
|
resource: source,
|
|
words: ["Write-Output", "tab\tnewline\n\u{1f642}", "left;right"],
|
|
rawWords: ['"Wr`ite-Output"', '"tab`tnewline`n`u{1f642}"', "left`;right"],
|
|
},
|
|
],
|
|
})
|
|
})
|
|
|
|
test("preserves word offsets relative to each raw command, including nested invocations", () => {
|
|
const source = ' & git\tstatus\t--short; Write-Output "$(git\tlog)"'
|
|
const result = ShellScan.scanPowerShell(source)
|
|
expect(result.kind).toBe("scanned")
|
|
if (result.kind === "opaque") return
|
|
expect(result.commands[0]?.wordEnds).toEqual([5, 12, 20])
|
|
expect(result.commands[2]?.resource).toBe("git\tlog")
|
|
for (const command of result.commands) {
|
|
expect(command.wordEnds).toHaveLength(command.rawWords.length)
|
|
for (const [index, word] of command.rawWords.entries()) {
|
|
const end = command.wordEnds![index]!
|
|
expect(command.resource.slice(end - word.length, end)).toBe(word)
|
|
}
|
|
}
|
|
})
|
|
|
|
test.each([
|
|
["ForEach-Object { git status }", true],
|
|
["git status; ForEach-Object { git diff }", true],
|
|
["git status | ForEach-Object { git diff }", undefined],
|
|
["git status && ForEach-Object { git diff }", undefined],
|
|
["git status || ForEach-Object { git diff }", undefined],
|
|
["& ForEach-Object { git status }", undefined],
|
|
["& 'ForEach-Object' { git status }", undefined],
|
|
["% { git status }", true],
|
|
["foreachthing { git status }", true],
|
|
] as const)("retains caller and lexical statement-head context: %s", (source, statementHead) => {
|
|
const result = ShellScan.scanPowerShell(source)
|
|
expect(result.kind).toBe("scanned")
|
|
if (result.kind === "opaque") return
|
|
const caller = result.commands.find((command) => command.rawWords.at(-1)?.startsWith("{"))
|
|
expect(caller).toBeDefined()
|
|
expect(caller?.statementHead).toBe(statementHead)
|
|
expect(result.commands.at(-1)?.words[0]).toBe("git")
|
|
})
|
|
|
|
test.each([
|
|
"if ($true) { git status",
|
|
'Write-Output "$(git status"',
|
|
"Write-Output @(git status",
|
|
"Write-Output @'\nunclosed",
|
|
"<# unclosed",
|
|
"git status |\n# no operand",
|
|
"Write-Output `u{110000}",
|
|
"Write-Output `u{xyz}",
|
|
])("bounds and reports incomplete lexical structures: %s", (source) => {
|
|
expect(ShellScan.scanPowerShell(source).kind).toBe("opaque")
|
|
})
|
|
|
|
test("bounds mixed expression, string, and block recursion", () => {
|
|
expect(ShellScan.scanPowerShell("$(".repeat(40) + "git status" + ")".repeat(40)).kind).toBe("opaque")
|
|
expect(ShellScan.scanPowerShell('Write-Output "$('.repeat(40) + "git status" + ')"'.repeat(40)).kind).toBe("opaque")
|
|
})
|
|
})
|