vize_canon 0.157.0

Canon - The standard of correctness for Vize type checking
Documentation
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
//! Helper functions and constants for virtual TypeScript generation.
//!
//! Contains utility functions for type declarations, event type mapping,
//! identifier conversion, and template context generation.

use std::ops::Range;

use super::types::VirtualTsOptions;
use vize_carton::String;
use vize_carton::append;
use vize_croquis::macros::{
    DEFINE_EMITS, DEFINE_EXPOSE, DEFINE_MODEL, DEFINE_PROPS, DEFINE_SLOTS, WITH_DEFAULTS,
};

pub(crate) const USE_TEMPLATE_REF: &str = "useTemplateRef";

/// Names declared by the generated setup-scope helper block.
///
/// This includes Vue compiler macros plus runtime helper shims that are modeled
/// inside `__setup()`. It is intentionally broader than `COMPILER_MACRO_NAMES`.
pub(crate) const SETUP_SCOPE_HELPER_NAMES: &[&str] = &[
    DEFINE_PROPS,
    DEFINE_EMITS,
    DEFINE_EXPOSE,
    DEFINE_MODEL,
    DEFINE_SLOTS,
    WITH_DEFAULTS,
    USE_TEMPLATE_REF,
];

/// Shared type helpers used by generated virtual modules and setup-scope macros.
pub(crate) const VUE_TYPE_HELPERS: &str = r#"type __EmitShape<T> = T extends (...args: any[]) => any ? T : T extends Record<string, any> ? { [K in keyof T]: T[K] extends (...args: infer A) => any ? A : T[K] extends any[] ? T[K] : any[]; } : Record<string, any[]>;
type __EmitArgs<T, K extends keyof T> = T[K] extends any[] ? T[K] : any[];
type __EmitFn<T> = __EmitShape<T> extends (...args: any[]) => any ? __EmitShape<T> : (<K extends keyof __EmitShape<T>>(event: K, ...args: __EmitArgs<__EmitShape<T>, K>) => void);
type __RuntimePropCtor<T> = T extends readonly (infer U)[] ? __RuntimePropCtor<U> : T extends { type: infer U } ? __RuntimePropCtor<U> : T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends ArrayConstructor ? unknown[] : T extends ObjectConstructor ? Record<string, unknown> : T extends DateConstructor ? Date : T extends FunctionConstructor ? (...args: any[]) => any : unknown;
type __RuntimePropResolved<T> = T extends { required: true } ? true : T extends { default: any } ? true : false;
type __RuntimePropShape<T extends Record<string, any>> = { [K in keyof T]: __RuntimePropResolved<T[K]> extends true ? __RuntimePropCtor<T[K]> : __RuntimePropCtor<T[K]> | undefined; };
type __DefaultFactory<T> = (props: any) => T;
type __WithDefaultValue<T> = T | __DefaultFactory<T>;
type __WithDefaultsArgs<T> = { [K in keyof T]?: __WithDefaultValue<T[K]> };
type __WithDefaultsResult<T, D extends __WithDefaultsArgs<T>> = Omit<T, keyof D> & { [K in keyof D & keyof T]-?: T[K] };
type __Ref<T> = import('vue').Ref<T>;
type __ShallowRef<T> = import('vue').ShallowRef<T>;
declare function __vForList<T>(source: readonly T[] | undefined | null): readonly [item: T, key: number, index: number][];
declare function __vForList(source: number | undefined | null): readonly [item: number, key: number, index: number][];
declare function __vForList(source: string | undefined | null): readonly [item: string, key: number, index: number][];
declare function __vForList<T>(source: Iterable<T> | undefined | null): readonly [item: T, key: number, index: number][];
declare function __vForList<T extends object>(source: T | undefined | null): readonly [item: T[keyof T], key: keyof T, index: number][];"#;

/// Vue setup-scope helpers - these are defined inside setup scope, NOT globally.
/// Compiler macros stay setup-only, while runtime helper shims model Vue APIs.
/// Parameters and type parameters are prefixed with _ to avoid "unused" warnings.
pub(crate) const VUE_SETUP_HELPERS: &str = r#"  // Compiler macros (only valid in setup scope, not global)
  function defineProps<_T = unknown>(): _T;
  function defineProps<const _T extends readonly string[]>(_props: _T): { [K in _T[number]]?: any };
  function defineProps<const _T extends Record<string, any>>(_props: _T): __RuntimePropShape<_T>;
  function defineProps(_props?: any) { void _props; return undefined as any; }
  function defineEmits<_T = unknown>(): __EmitFn<_T>;
  function defineEmits<const _T extends readonly string[]>(_events: _T): (event: _T[number], ...args: any[]) => void;
  function defineEmits<const _T extends Record<string, any>>(_events: _T): __EmitFn<_T>;
  function defineEmits(_events?: any) { void _events; return (() => {}) as any; }
  function defineExpose<_T = unknown>(_exposed?: _T): void { void _exposed; }
  function defineModel<_T = unknown>(): __Ref<_T | undefined>;
  function defineModel<_T = unknown>(_options: any): __Ref<_T>;
  function defineModel<_T = unknown>(_name: string, _options?: any): __Ref<_T>;
  function defineModel(_name_or_options?: any, _options?: any) { void _name_or_options; void _options; return undefined as any; }
  function defineSlots<_T = unknown>(): _T { return undefined as unknown as _T; }
  function withDefaults<_T, _D extends __WithDefaultsArgs<_T>>(_props: _T, _defaults: _D): __WithDefaultsResult<_T, _D> { void _props; void _defaults; return undefined as unknown as __WithDefaultsResult<_T, _D>; }
  function useTemplateRef<_T = any>(_key: string): __ShallowRef<_T | null> { void _key; return undefined as unknown as __ShallowRef<_T | null>; }
  // Mark compiler macros as used
  void defineProps; void defineEmits; void defineExpose; void defineModel; void defineSlots; void withDefaults; void useTemplateRef;"#;

/// ImportMeta augmentation for Vite/Nuxt projects.
/// Uses `/// <reference types="..." />` to pull in existing type definitions
/// from frameworks like Vite, Nuxt, etc. when available.
pub(crate) const IMPORT_META_AUGMENTATION: &str = r#"// ImportMeta augmentation (reference existing framework types)
/// <reference types="vite/client" />
declare global {
  // Extend ImportMeta with Nuxt-specific properties not covered by vite/client
  interface ImportMeta {
    readonly client: boolean;
    readonly server: boolean;
    readonly dev: boolean;
    readonly prod: boolean;
    readonly ssr: boolean;
  }
}
"#;

/// Generate Vue template context declarations dynamically.
///
/// Derives `$`-prefixed globals from `ComponentPublicInstance` so that
/// type resolution is delegated to Corsa via Vue's type system
/// (including `ComponentCustomProperties` augmentations from plugins).
pub(crate) fn generate_template_context(options: &VirtualTsOptions) -> String {
    let mut ctx = String::default();

    let needs_global_helper =
        !options.template_globals.is_empty() || !options.css_modules.is_empty();

    // Instance type + conditional accessor helper
    ctx.push_str("    // Vue template context (delegates to ComponentPublicInstance)\n");
    ctx.push_str("    type __Ctx = import('vue').ComponentPublicInstance;\n");
    if needs_global_helper {
        ctx.push_str("    type __Global<K extends string, F = unknown> = K extends keyof __Ctx ? __Ctx[K] : F;\n");
    }
    ctx.push_str("    const __ctx = undefined as unknown as __Ctx;\n");

    // Core Vue globals (always present on ComponentPublicInstance)
    ctx.push_str("    const $attrs = __ctx.$attrs;\n");
    ctx.push_str("    const $slots = __ctx.$slots;\n");
    ctx.push_str("    const $refs = __ctx.$refs;\n");
    ctx.push_str("    const $emit = __ctx.$emit;\n");

    // Plugin globals (resolved via ComponentCustomProperties if augmented,
    // otherwise falls back to the configured type_annotation)
    if !options.template_globals.is_empty() {
        ctx.push_str("    // Plugin globals (via ComponentCustomProperties)\n");
        for global in &options.template_globals {
            append!(
                ctx,
                "    const {}: __Global<'{}', {}> = undefined as any;\n",
                global.name,
                global.name,
                global.type_annotation
            );
        }
    }

    // CSS module globals (resolved via ComponentCustomProperties if augmented,
    // otherwise falls back to Record<string, string>)
    if !options.css_modules.is_empty() {
        ctx.push_str("    // CSS modules (from <style module>)\n");
        for module_name in &options.css_modules {
            append!(
                ctx,
                "    const {module_name}: __Global<'{module_name}', Record<string, string>> = undefined as any;\n"
            );
        }
    }

    // Mark all as used
    ctx.push_str("    void __ctx; void $attrs; void $slots; void $refs; void $emit;\n");
    if !options.template_globals.is_empty() {
        ctx.push_str("    ");
        for (i, global) in options.template_globals.iter().enumerate() {
            if i > 0 {
                ctx.push(' ');
            }
            append!(ctx, "void {};", global.name);
        }
        ctx.push('\n');
    }
    if !options.css_modules.is_empty() {
        ctx.push_str("    ");
        for (i, module_name) in options.css_modules.iter().enumerate() {
            if i > 0 {
                ctx.push(' ');
            }
            append!(ctx, "void {module_name};");
        }
        ctx.push('\n');
    }

    ctx
}

/// Get the generated subrange that corresponds to a specific source expression.
///
/// This keeps source maps anchored to the actual expression text instead of
/// any wrapping code we emit around it (`void (...)`, `as Foo`, handler shims).
pub(crate) fn generated_text_range(
    generated_segment: &str,
    mapped_text: &str,
    generated_start: usize,
) -> Range<usize> {
    if mapped_text.is_empty() {
        return generated_start..generated_start + generated_segment.len();
    }

    let relative_start = generated_segment.find(mapped_text).unwrap_or(0);
    let start = generated_start + relative_start;
    start..start + mapped_text.len()
}

/// Get the TypeScript event type for a DOM event name.
/// Returns the specific event interface (MouseEvent, KeyboardEvent, etc.)
pub(crate) fn get_dom_event_type(event_name: &str) -> &'static str {
    match event_name {
        // Mouse events
        "click" | "dblclick" | "mousedown" | "mouseup" | "mousemove" | "mouseenter"
        | "mouseleave" | "mouseover" | "mouseout" | "contextmenu" => "MouseEvent",

        // Pointer events
        "pointerdown" | "pointerup" | "pointermove" | "pointerenter" | "pointerleave"
        | "pointerover" | "pointerout" | "pointercancel" | "gotpointercapture"
        | "lostpointercapture" => "PointerEvent",

        // Touch events
        "touchstart" | "touchend" | "touchmove" | "touchcancel" => "TouchEvent",

        // Keyboard events
        "keydown" | "keyup" | "keypress" => "KeyboardEvent",

        // Focus events
        "focus" | "blur" | "focusin" | "focusout" => "FocusEvent",

        // Input events
        "input" | "beforeinput" => "InputEvent",

        // Composition events
        "compositionstart" | "compositionend" | "compositionupdate" => "CompositionEvent",

        // Form events
        "submit" => "SubmitEvent",
        "change" => "Event",
        "reset" => "Event",

        // Drag events
        "drag" | "dragstart" | "dragend" | "dragenter" | "dragleave" | "dragover" | "drop" => {
            "DragEvent"
        }

        // Clipboard events
        "cut" | "copy" | "paste" => "ClipboardEvent",

        // Wheel events
        "wheel" => "WheelEvent",

        // Animation events
        "animationstart" | "animationend" | "animationiteration" | "animationcancel" => {
            "AnimationEvent"
        }

        // Transition events
        "transitionstart" | "transitionend" | "transitionrun" | "transitioncancel" => {
            "TransitionEvent"
        }

        // UI events
        "scroll" | "resize" => "Event",

        // Media events
        "play" | "pause" | "ended" | "loadeddata" | "loadedmetadata" | "timeupdate"
        | "volumechange" | "waiting" | "seeking" | "seeked" | "ratechange" | "durationchange"
        | "canplay" | "canplaythrough" | "playing" | "progress" | "stalled" | "suspend"
        | "emptied" | "abort" => "Event",

        // Error/Load events
        "error" => "ErrorEvent",
        "load" => "Event",

        // Selection events
        "select" | "selectionchange" | "selectstart" => "Event",

        // Modern UI events that were absent — without an entry, `@toggle` /
        // `@beforetoggle` etc. fell back to the bare `Event` interface and
        // the user lost the specific payload members. See #688.
        "toggle" | "beforetoggle" => "ToggleEvent",
        "formdata" => "FormDataEvent",
        "popstate" => "PopStateEvent",
        "hashchange" => "HashChangeEvent",
        "message" => "MessageEvent",
        "storage" => "StorageEvent",
        "online" | "offline" => "Event",
        "securitypolicyviolation" => "SecurityPolicyViolationEvent",

        // Default fallback
        _ => "Event",
    }
}

#[cfg(test)]
mod event_type_tests {
    use super::get_dom_event_type;

    #[test]
    fn maps_legacy_dom_events() {
        assert_eq!(get_dom_event_type("click"), "MouseEvent");
        assert_eq!(get_dom_event_type("keydown"), "KeyboardEvent");
        assert_eq!(get_dom_event_type("submit"), "SubmitEvent");
    }

    #[test]
    fn maps_modern_dom_events() {
        // These fell back to `Event` before #688 — now they get the specific
        // interface so `e.newState` / `e.formData` etc. complete.
        assert_eq!(get_dom_event_type("toggle"), "ToggleEvent");
        assert_eq!(get_dom_event_type("beforetoggle"), "ToggleEvent");
        assert_eq!(get_dom_event_type("formdata"), "FormDataEvent");
    }

    #[test]
    fn unknown_events_fall_back_to_event() {
        assert_eq!(get_dom_event_type("totally-made-up"), "Event");
    }
}

/// Convert kebab-case or PascalCase prop name to camelCase.
/// Vue normalizes prop names to camelCase internally.
/// Examples: "my-prop" -> "myProp", "MyProp" -> "myProp"
pub(crate) fn to_camel_case(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut capitalize_next = false;
    let mut first = true;

    for c in s.chars() {
        if c == '-' || c == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_ascii_uppercase());
            capitalize_next = false;
        } else if first {
            // First character should be lowercase
            result.push(c.to_ascii_lowercase());
            first = false;
        } else {
            result.push(c);
        }
    }

    result
}

/// Sanitize a string to be a valid TypeScript identifier.
/// Replaces invalid characters (like ':') with underscores and prefixes
/// reserved words.
/// Examples: "update:title" -> "update_title", "my-event" -> "my_event"
pub(crate) fn to_safe_identifier(s: &str) -> String {
    let mut result = to_safe_identifier_fragment(s);

    if !result
        .chars()
        .next()
        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
    {
        result.insert(0, '_');
    }
    if is_reserved_identifier(result.as_str()) {
        result.insert(0, '_');
    }

    result
}

/// Sanitize a string for use inside a generated identifier that already has a
/// safe prefix (for example `_slot_{name}`).
pub(crate) fn to_safe_identifier_fragment(s: &str) -> String {
    let mut result = String::with_capacity(s.len().max(1));

    for c in s.chars() {
        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
            result.push(c);
        } else {
            result.push('_');
        }
    }

    if result.is_empty() {
        result.push('_');
    }

    result
}

#[inline]
pub(crate) fn is_reserved_identifier(s: &str) -> bool {
    matches!(
        s,
        "await"
            | "break"
            | "case"
            | "catch"
            | "class"
            | "const"
            | "continue"
            | "debugger"
            | "default"
            | "delete"
            | "do"
            | "else"
            | "enum"
            | "export"
            | "extends"
            | "false"
            | "finally"
            | "for"
            | "function"
            | "if"
            | "import"
            | "in"
            | "instanceof"
            | "new"
            | "null"
            | "return"
            | "super"
            | "switch"
            | "this"
            | "throw"
            | "true"
            | "try"
            | "typeof"
            | "var"
            | "void"
            | "while"
            | "with"
            | "yield"
            | "let"
            | "static"
            | "implements"
            | "interface"
            | "package"
            | "private"
            | "protected"
            | "public"
            | "as"
            | "from"
            | "of"
    )
}