All files print-type.ts

45.23% Statements 38/84
42.55% Branches 40/94
40% Functions 4/10
47.14% Lines 33/70

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            34x   23x 23x         23x 23x 23x 23x 23x                         1x                   10x                                                             4x 4x 4x 4x 4x 4x 4x 4x 4x       4x 4x 4x                   4x 4x 4x 4x       4x                       5x   2x 1x                       4x     4x             4x    
import type { ContractTypeNode, FieldNode, InlineObjectTypeNode } from '@contractkit/core';
import { INDENT } from './indent.js';
 
// ─── Type expression printer ────────────────────────────────────────────────
 
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.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}${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 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;
}