Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 36x 23x 23x 23x 23x 23x 23x 23x 3x 10x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 9x 2x 5x 2x 1x 6x 6x 6x | import type { ContractTypeNode, FieldNode, InlineObjectTypeNode } from '@contractkit/core';
import { INDENT } from './indent.js';
// ─── Type expression printer ────────────────────────────────────────────────
/** Render a `ContractTypeNode` back to its `.ck` source string. */
export function printType(type: ContractTypeNode): string {
switch (type.kind) {
case 'scalar': {
const constraints: string[] = [];
Iif (type.format !== undefined) {
// Print unquoted when format contains only safe chars; quote otherwise
const fmt = type.format;
constraints.push(/^[a-zA-Z0-9\-.:/]+$/.test(fmt) ? fmt : `"${fmt}"`);
}
if (type.min !== undefined) constraints.push(`min=${type.min}`);
if (type.max !== undefined) constraints.push(`max=${type.max}`);
Iif (type.len !== undefined) constraints.push(`len=${type.len}`);
if (type.regex !== undefined) constraints.push(`regex=/${type.regex}/`);
return constraints.length > 0 ? `${type.name}(${constraints.join(', ')})` : type.name;
}
case 'array': {
const args: string[] = [printType(type.item)];
if (type.min !== undefined) args.push(`min=${type.min}`);
if (type.max !== undefined) args.push(`max=${type.max}`);
return `array(${args.join(', ')})`;
}
case 'tuple':
return `tuple(${type.items.map(printType).join(', ')})`;
case 'record':
return `record(${printType(type.key)}, ${printType(type.value)})`;
case 'enum':
return `enum(${type.values.map(formatEnumValue).join(', ')})`;
case 'literal':
return typeof type.value === 'string' ? `literal("${type.value}")` : `literal(${type.value})`;
case 'union':
return type.members.map(printType).join(' | ');
case 'discriminatedUnion':
return `discriminated(by=${type.discriminator}, ${type.members.map(printType).join(' | ')})`;
case 'intersection':
return type.members.map(printType).join(' & ');
case 'ref':
return type.name;
case 'inlineObject':
return printInlineObjectCompact(type);
case 'lazy':
return `lazy(${printType(type.inner)})`;
}
}
/** Compact single-line form — used when inline object appears nested inside another type. */
function printInlineObjectCompact(obj: InlineObjectTypeNode): string {
const prefix = obj.mode ? `mode(${obj.mode}) ` : '';
if (obj.fields.length === 0) return `${prefix}{}`;
const parts = obj.fields.map(f => {
const opt = f.optional ? '?' : '';
let t = printType(f.type);
if (f.nullable) t += ' | null';
return `${f.name}${opt}: ${t}`;
});
return `${prefix}{ ${parts.join(', ')} }`;
}
/** Multi-line enum form — one value per line, used when single-line would exceed print width. */
export function printEnumExpanded(values: string[], indent: string): string {
const innerIndent = indent + INDENT;
return `enum(\n${values.map(v => `${innerIndent}${formatEnumValue(v)}`).join(',\n')}\n${indent})`;
}
// ─── Field printer ──────────────────────────────────────────────────────────
/** Print a full field declaration, including visibility, default, and inline comment.
* Modifier order is canonical: override → deprecated → readonly|writeonly → type. */
export function printField(field: FieldNode, indent: string, printWidth: number = 80): string {
const opt = field.optional ? '?' : '';
const ovr = field.override ? 'override ' : '';
const dep = field.deprecated ? 'deprecated ' : '';
const vis = field.visibility !== 'normal' ? `${field.visibility} ` : '';
const mods = `${ovr}${dep}${vis}`;
const def = field.default !== undefined ? ` = ${formatDefault(field.default)}` : '';
const comment = field.description ? ` # ${field.description}` : '';
const innerIndent = indent + INDENT;
// Expand inline object types to multi-line — same rule as type aliases.
// Only when there's no default and no nullable union (those can't split cleanly).
Eif (!field.nullable && field.default === undefined) {
const trailing = extractTrailingInlineObject(field.type);
Iif (trailing) {
const { prefix, inlineObj } = trailing;
const modePart = inlineObj.mode ? `mode(${inlineObj.mode}) ` : '';
const header = prefix
? `${indent}${field.name}${opt}: ${mods}${prefix} & ${modePart}{${comment}`
: `${indent}${field.name}${opt}: ${mods}${modePart}{${comment}`;
return [header, ...printInlineObjectExpanded(inlineObj, innerIndent, printWidth), `${indent}}`].join('\n');
}
}
let typeStr = printType(field.type);
Iif (field.nullable) typeStr += ' | null';
const fullLine = `${indent}${field.name}${opt}: ${mods}${typeStr}${def}${comment}`;
Iif (field.type.kind === 'enum' && !field.nullable && field.default === undefined && fullLine.length > printWidth) {
const enumStr = printEnumExpanded(field.type.values, indent);
return `${indent}${field.name}${opt}: ${mods}${enumStr}${comment}`;
}
return fullLine;
}
/** Print inline-object fields expanded (used when an inline brace object trails a type alias). */
export function printInlineObjectExpanded(obj: InlineObjectTypeNode, indent: string, printWidth: number = 80): string[] {
return obj.fields.map(f => printField(f, indent, printWidth));
}
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Format a single enum value: bare identifier stays bare; anything else gets double-quoted. */
export function formatEnumValue(v: string): string {
if (/^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(v)) return v;
return `"${v}"`;
}
/** Format a default value: quote strings that aren't valid bare identifiers. */
export function formatDefault(val: string | number | boolean): string {
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
// If it looks like a bare identifier (enum value, unquoted token), keep it bare.
if (/^[a-zA-Z_$][a-zA-Z0-9_$\-.]*$/.test(val)) return val;
return `"${val}"`;
}
/**
* Detect whether the last member of a type is an inline brace object, and if so
* return the prefix type string and the inline object for expanded printing.
* Returns null if the type doesn't end with an inline object.
*/
export function extractTrailingInlineObject(type: ContractTypeNode): {
prefix: string | null;
inlineObj: InlineObjectTypeNode;
} | null {
Iif (type.kind === 'inlineObject') {
return { prefix: null, inlineObj: type };
}
Iif (type.kind === 'intersection') {
const last = type.members[type.members.length - 1];
if (last?.kind === 'inlineObject') {
const prefixStr = type.members.slice(0, -1).map(printType).join(' & ');
return { prefix: prefixStr, inlineObj: last };
}
}
return null;
}
|