Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,9 +388,17 @@ function arithExpr(e: ArithmeticExpression): string {
return e.value;
case "ArithmeticGroup":
return "(" + arithExpr(e.expression) + ")";
case "ArithmeticUnary":
if (e.prefix) return (e.operator === "!" ? "! " : e.operator) + arithExpr(e.operand);
return arithExpr(e.operand) + e.operator;
case "ArithmeticUnary": {
const operand = arithExpr(e.operand);
if (e.prefix) {
const separator =
e.operator === "!" || ((e.operator === "+" || e.operator === "-") && operand.startsWith(e.operator))
? " "
: "";
return e.operator + separator + operand;
}
return operand + e.operator;
}
case "ArithmeticTernary":
return arithExpr(e.test) + " ? " + arithExpr(e.consequent) + " : " + arithExpr(e.alternate);
case "ArithmeticBinary": {
Expand Down
74 changes: 74 additions & 0 deletions test/printer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,62 @@ test("arithmetic unary operators retain their spelling", () => {
assert.equal(fmt("(( ~ 1 + - 2 + + 3 + ++ i + j -- ))"), "(( ~1 + -2 + +3 + ++i + j-- ))");
});

for (const operator of ["-", "+"]) {
test(`arithmetic nested unary ${operator} preserves both operators`, () => {
const printed = fmt(`(( ${operator} ${operator}x ))`);
const reparsed = parse(printed);
const command = nodeOfType(reparsed.commands[0].command, "ArithmeticCommand");
const outer = nodeOfType(command.expression, "ArithmeticUnary");
assert.equal(outer.operator, operator);
assert.equal(outer.prefix, true);
const inner = nodeOfType(outer.operand, "ArithmeticUnary");
assert.equal(inner.operator, operator);
assert.equal(inner.prefix, true);
assert.equal(nodeOfType(inner.operand, "ArithmeticWord").value, "x");
assert.equal(reparsed.errors, undefined);
});
}

test("arithmetic unary token boundaries survive printing and reparsing", () => {
const shape = (value: unknown) =>
JSON.stringify(value, (key, value) => (key === "pos" || key === "end" ? undefined : value));
for (const [expression, expected] of [
["- - -x", "- - -x"],
["+ + +x", "+ + +x"],
["- --x", "- --x"],
["+ ++x", "+ ++x"],
["- -x--", "- -x--"],
["+ +x++", "+ +x++"],
["- -2", "- -2"],
["+ +2", "+ +2"],
["- +x", "-+x"],
["+ -x", "+-x"],
["- ++x", "-++x"],
["+ --x", "+--x"],
["++ x", "++x"],
["-- x", "--x"],
["x ++", "x++"],
["x --", "x--"],
["-(-x)", "-(-x)"],
["+(+x)", "+(+x)"],
["~ - -x", "~- -x"],
["! + +x", "! + +x"],
]) {
const original = parse(`(( ${expression} ))`);
const printed = print(original);
assert.equal(printed, `(( ${expected} ))`, expression);
const reparsed = parse(printed);
assert.equal(
shape(nodeOfType(reparsed.commands[0].command, "ArithmeticCommand").expression),
shape(nodeOfType(original.commands[0].command, "ArithmeticCommand").expression),
expression,
);
assert.equal(original.errors, undefined, expression);
assert.equal(reparsed.errors, undefined, expression);
assert.equal(print(reparsed), printed, expression);
}
});

// --- Arithmetic for ---

test("arithmetic for loop", () => {
Expand All @@ -337,6 +393,24 @@ test("arithmetic for negation keeps history expansion inactive in every expressi
assert.equal(fmt(expected), expected);
});

test("arithmetic for preserves unary token boundaries in every expression", () => {
const source = "for ((i=- -2; + +i<4; i+=+ +1)); do :; done";
const original = parse(source);
const printed = print(original);
assert.equal(printed, "for (( i = - -2; + +i < 4; i += + +1 )); do\n :\ndone");
const reparsed = parse(printed);
const before = nodeOfType(original.commands[0].command, "ArithmeticFor");
const after = nodeOfType(reparsed.commands[0].command, "ArithmeticFor");
const shape = (value: unknown) =>
JSON.stringify(value, (key, value) => (key === "pos" || key === "end" ? undefined : value));
for (const field of ["initialize", "test", "update"] as const) {
assert.equal(shape(after[field]), shape(before[field]), field);
}
assert.equal(original.errors, undefined);
assert.equal(reparsed.errors, undefined);
assert.equal(print(reparsed), printed);
});

// --- Coproc ---

test("coproc with name", () => {
Expand Down