All files print-type.ts

47.12% Statements 41/87
46.87% Branches 45/96
45.45% Functions 5/11
48.61% Lines 35/72

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;
}