@devaloop/prettier-plugin-devalang
Version:
Prettier plugin for Devalang formatting
222 lines (168 loc) • 5.88 kB
text/typescript
import { type AstPath, type Doc, doc } from "prettier";
import { Expression } from "./types/expression";
import { Node } from "./types/node";
import { ArrowCallStatement } from "./interfaces/statement";
import { parseBlock } from "./parser";
const { hardline, indent, join, group } = doc.builders;
/**
* Prints an expression by its type.
* @param expr Expression
* @returns string
*/
const printExpression = (expr: Expression): string => {
switch (expr.type) {
case "Identifier":
return expr.name;
case "NumberLiteral":
return expr.value.toString();
case "StringLiteral":
return `"${expr.value}"`;
case "BooleanLiteral":
return expr.value ? "true" : "false";
case "ObjectLiteral":
if (!expr.properties || expr.properties.length === 0) {
return "";
}
return `{ ${expr.properties
.map((p: any) => `${p.key ? p.key + ':' : ''} ${printExpression(p.value)}`)
.join(", ")} }`;
case "ObjectProperty":
return `${expr.key}: ${printExpression(expr.value)}`;
case "SynthReference":
return `synth ${expr.name}`;
default:
throw new Error(`Unsupported expression type: ${(expr as any).type}`);
}
};
/**
* Prints a node matching its type.
* @param path AstPath<Node>
* @param options any
* @param print (path: AstPath<Node>) => Doc
* @returns Doc
*/
export const print = (
path: AstPath<Node>,
options: any,
print: (path: AstPath<Node>) => Doc
): Doc => {
const node = path.node ? path.node : path.getValue();
// NOTE: See the parsed AST in the console for debugging purposes.
if (node.type === "Program") {
console.log("\nPrettier AST :");
console.dir(node, { depth: null, colors: true });
console.log();
}
switch (node.type) {
case "Program": {
const printedNodes = path.map(print, "body");
const parts: Doc[] = [];
for (let i = 0; i < node.body.length; i++) {
const printed = printedNodes[i];
if (printed === "" || printed === undefined) continue;
parts.push(printed);
const nextNode = node.body[i + 1];
if (!nextNode || nextNode.type === "BlankLine") continue;
parts.push(hardline);
}
while (parts.length > 0 && parts[parts.length - 1] === hardline) {
parts.pop();
}
return parts;
}
case "BpmDeclaration":
return `bpm ${node.identifier}`;
case "BankDeclaration":
return `bank ${node.identifier}`;
case "LetDeclaration":
return `let ${node.name} = ${printExpression(node.value)}`;
case "Loop": {
const parts: Doc[] = parseBlock(node.body);
return group([
`loop ${node.iterator.value}:`,
indent([hardline, ...parts])
]);
}
case "Trigger":
return `${node.name} ${node.args.length && node.duration ? node.duration.value + " " : node.duration ? node.duration.value : ""}${printArguments(node.args)}`;
case "ImportStatement":
return `@import { ${node.identifiers.join(", ")} } from "${node.from}"`;
case "ExportStatement":
return `@export { ${node.identifiers.join(", ")} }`;
case "LoadSample":
return `@load "${node.path}" as ${node.alias}`;
case "Group": {
const rawChildren = node.body;
const parts: Doc[] = parseBlock(rawChildren);
return ["group ", node.name, ":", indent([hardline, ...parts])];
}
case "Call":
return `call ${node.identifier}`;
case "Sleep":
return `sleep ${node.value}`;
case "Spawn":
return `spawn ${node.identifier}`;
case "If": {
const parts: Doc[] = [];
// if
parts.push(`if ${node.condition}:`);
parts.push(indent([hardline, ...parseBlock(node.body)]));
// else ifs
for (const elseIf of node.elseIfs ?? []) {
parts.push(hardline, `else if ${elseIf.condition}:`);
parts.push(indent([hardline, ...parseBlock(elseIf.body)]));
}
// else
if (node.alternate && node.alternate.length > 0) {
parts.push(hardline, "else:");
parts.push(indent([hardline, ...parseBlock(node.alternate)]));
}
return group(parts);
}
case "Comment":
return node.value;
case "Unknown":
return node.value;
case "BlankLine":
return hardline;
case "ArrowCall":
return printArrowCall(node);
default:
throw new Error(`Unsupported node type: ${(node as any).type}`);
}
};
const printArrowCall = (call: ArrowCallStatement): Doc => {
const target = call.target;
const func = call.method;
const args: Expression[] = call.args
.filter((arg) => !(arg.type === "ObjectLiteral" && arg.properties.length === 0));
if (args.length === 0) {
return `${target} -> ${func}()`;
}
return group([
`${target} -> ${func}(`,
join(", ", args.map(printExpression)),
")"
]);
};
const printArguments = (args: Expression[]): string => {
if (args.length === 0) return "";
const isAllObjectProps = args.every(arg => arg.type === "ObjectProperty");
if (isAllObjectProps) {
const props = args as any[];
const printedProps = props
.map((p) => `${p.key}: ${printExpression(p.value)}`)
.join(", ");
return printedProps ? `{ ${printedProps} }` : "";
}
if (args.length === 1 && args[0].type === "Identifier") {
return args[0].name;
}
return args.map(printExpression).join(", ");
};
export const embed = undefined;
export const insertPragma = undefined;
export const massageAstNode = undefined;
export const hasPragma = undefined;
export const preprocess = undefined;
export const astFormat = "devalang";