vize_canon 0.29.0

Canon - The standard of correctness for Vize type checking
Documentation
//! Virtual TypeScript generator for Vue SFC.
//!
//! Generates pure TypeScript from Vue SFC for tsgo type checking.

use super::import_rewriter::ImportRewriter;
use super::source_map::SfcSourceMap;
use super::SfcBlockType;
use vize_atelier_sfc::SfcDescriptor;
use vize_carton::append;
use vize_carton::cstr;
use vize_carton::String;
use vize_croquis::{Analyzer, AnalyzerOptions, Croquis};

/// Result of virtual TypeScript generation.
#[derive(Debug)]
pub struct VirtualTsResult {
    /// Generated TypeScript code.
    pub code: String,
    /// Source map for position mapping.
    pub source_map: SfcSourceMap,
}

/// Vue compiler macros - defined with parameters marked as used.
const VUE_SETUP_COMPILER_MACROS: &str = r#"// Compiler macros (transformed at compile time by Vue)
function defineProps<T>(): T { return undefined as unknown as T; }
type __BatchEmitFn<T> = T extends (...args: any[]) => any ? T : (<K extends keyof T>(event: K, ...args: T[K] extends any[] ? T[K] : any[]) => void);
function defineEmits<T>(): __BatchEmitFn<T> { return (() => {}) as any; }
function defineEmits<T extends readonly string[]>(_events: T): (event: T[number], ...args: any[]) => void { void _events; return (() => {}) as any; }
function defineEmits<T extends Record<string, any>>(_events: T): (event: keyof T, ...args: any[]) => void { void _events; return (() => {}) as any; }
function defineExpose<T>(_exposed?: T): void { void _exposed; }
function defineModel<T>(): $Vue['Ref']<T | undefined> { return undefined as unknown as $Vue['Ref']<T | undefined>; }
function defineModel<T>(_options: any): $Vue['Ref']<T> { void _options; return undefined as unknown as $Vue['Ref']<T>; }
function defineModel<T>(_name: string, _options?: any): $Vue['Ref']<T> { void _name; void _options; return undefined as unknown as $Vue['Ref']<T>; }
function defineSlots<T>(): T { return undefined as unknown as T; }
function withDefaults<T, D>(_props: T, _defaults: D): T & D { void _props; void _defaults; return undefined as unknown as T & D; }
function useTemplateRef<T = any>(_key: string): $Vue['ShallowRef']<T | null> { void _key; return undefined as unknown as $Vue['ShallowRef']<T | null>; }
// Mark compiler macros as used
void defineProps; void defineEmits; void defineExpose; void defineModel; void defineSlots; void withDefaults; void useTemplateRef;
"#;

/// Vue template context available in template expressions.
const VUE_TEMPLATE_CONTEXT: &str = r#"// Vue instance context (available in template)
const $attrs: Record<string, unknown> = {} as any;
const $slots: Record<string, (...args: any[]) => any> = {} as any;
const $refs: Record<string, any> = {} as any;
const $emit: (...args: any[]) => void = (() => {}) as any;
// Mark template context as used
void $attrs; void $slots; void $refs; void $emit;
"#;

/// Virtual TypeScript generator.
pub struct VirtualTsGenerator;

impl VirtualTsGenerator {
    /// Create a new generator.
    pub fn new() -> Self {
        Self
    }

    /// Generate virtual TypeScript from SFC descriptor.
    pub fn generate(&self, descriptor: &SfcDescriptor, analysis: &Croquis) -> VirtualTsResult {
        let mut code = String::default();
        let mut source_map = SfcSourceMap::new();

        // Header
        code.push_str("// ============================================\n");
        code.push_str("// Virtual TypeScript for Vue SFC Type Checking\n");
        code.push_str("// Generated by vize\n");
        code.push_str("// ============================================\n\n");

        // Vue type alias (shorthand for import('vue') references)
        code.push_str("type $Vue = import('vue');\n\n");

        // Compiler macros (module scope - ambient declarations)
        code.push_str(VUE_SETUP_COMPILER_MACROS);
        code.push('\n');

        // Template context (module scope - ambient declarations)
        code.push_str(VUE_TEMPLATE_CONTEXT);
        code.push('\n');

        // Module scope: Extract and emit imports (with .vue -> .vue.ts rewrite)
        code.push_str("// ========== Imports ==========\n");

        if let Some(ref script_setup) = descriptor.script_setup {
            self.emit_imports(
                &mut code,
                &script_setup.content,
                &mut source_map,
                &script_setup.loc,
            );
        } else if let Some(ref script) = descriptor.script {
            self.emit_imports(&mut code, &script.content, &mut source_map, &script.loc);
        }

        code.push('\n');

        // User's script content (excluding imports) - directly in module scope
        code.push_str("// ========== Script Content ==========\n");

        if let Some(ref script_setup) = descriptor.script_setup {
            let script_start = code.len() as u32;
            self.emit_script_content_module_scope(&mut code, &script_setup.content);
            let script_end = code.len() as u32;

            source_map.add_mapping(
                script_start,
                script_end,
                script_setup.loc.start as u32,
                SfcBlockType::ScriptSetup,
            );
        } else if let Some(ref script) = descriptor.script {
            let script_start = code.len() as u32;
            self.emit_script_content_module_scope(&mut code, &script.content);
            let script_end = code.len() as u32;

            source_map.add_mapping(
                script_start,
                script_end,
                script.loc.start as u32,
                SfcBlockType::Script,
            );
        }

        code.push('\n');

        // Template bindings (wrap in IIFE to check template expressions)
        if let Some(ref template) = descriptor.template {
            code.push_str("// ========== Template Bindings ==========\n");
            code.push_str("(function __template() {\n");

            let template_start = code.len() as u32;
            self.emit_template_bindings(&mut code, analysis);
            let template_end = code.len() as u32;

            source_map.add_mapping(
                template_start,
                template_end,
                template.loc.start as u32,
                SfcBlockType::Template,
            );

            code.push_str("})();\n\n");
        }

        // Component export
        code.push_str("// ========== Component Export ==========\n");
        code.push_str("import { DefineComponent } from 'vue';\n\n");

        // Generate Props/Emits interfaces from analysis
        self.emit_component_types(&mut code, analysis);

        code.push_str("declare const __component: DefineComponent<__Props, {}, {}, {}, {}, {}, {}, __Emits>;\n");
        code.push_str("export default __component;\n");

        VirtualTsResult { code, source_map }
    }

    /// Emit imports with .vue -> .vue.ts rewrite using OXC-based ImportRewriter.
    fn emit_imports(
        &self,
        code: &mut String,
        script: &str,
        _source_map: &mut SfcSourceMap,
        _loc: &vize_atelier_sfc::BlockLocation,
    ) {
        // Use ImportRewriter for AST-based import rewriting
        let rewriter = ImportRewriter::new();
        let rewrite_result = rewriter.rewrite(script, oxc_span::SourceType::ts());

        // Extract import lines from the rewritten code
        for line in rewrite_result.code.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with("import ") {
                code.push_str("  ");
                code.push_str(line);
                code.push('\n');
            }
        }
    }

    /// Emit script content excluding imports (module scope, no indentation).
    fn emit_script_content_module_scope(&self, code: &mut String, script: &str) {
        let mut first_content = true;
        for line in script.lines() {
            let trimmed = line.trim();
            // Skip import statements (already emitted)
            if trimmed.starts_with("import ") {
                continue;
            }
            // Skip empty lines at the very start
            if first_content && trimmed.is_empty() {
                continue;
            }
            first_content = false;
            code.push_str(line);
            code.push('\n');
        }
    }

    /// Emit template binding references.
    fn emit_template_bindings(&self, code: &mut String, analysis: &Croquis) {
        // Emit void statements for all bindings to trigger type checking
        for (name, _binding_type) in analysis.bindings.iter() {
            append!(*code, "    void {name};\n");
        }
    }

    /// Emit component type definitions.
    fn emit_component_types(&self, code: &mut String, analysis: &Croquis) {
        // Props interface
        code.push_str("export interface __Props {\n");
        for prop in analysis.macros.props() {
            let optional = if !prop.required { "?" } else { "" };
            let prop_type = prop.prop_type.as_deref().unwrap_or("unknown");
            append!(*code, "  {}{}: {};\n", prop.name, optional, prop_type);
        }
        code.push_str("}\n\n");

        // Emits interface
        code.push_str("export interface __Emits {\n");
        for emit in analysis.macros.emits() {
            if let Some(ref payload_type) = emit.payload_type {
                append!(
                    *code,
                    "  (e: '{}', payload: {}): void;\n",
                    emit.name,
                    payload_type
                );
            } else {
                append!(*code, "  (e: '{}'): void;\n", emit.name);
            }
        }
        code.push_str("}\n\n");
    }

    /// Generate virtual TypeScript from SFC content string.
    pub fn generate_from_content(&self, content: &str) -> Result<VirtualTsResult, String> {
        let options = vize_atelier_sfc::SfcParseOptions::default();
        let descriptor =
            vize_atelier_sfc::parse_sfc(content, options).map_err(|e| cstr!("{e:?}"))?;

        let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());

        if let Some(ref script_setup) = descriptor.script_setup {
            analyzer.analyze_script_setup(&script_setup.content);
        } else if let Some(ref script) = descriptor.script {
            analyzer.analyze_script_plain(&script.content);
        }

        if let Some(ref template) = descriptor.template {
            let allocator = vize_carton::Bump::new();
            let (root, _) = vize_armature::parse(&allocator, &template.content);
            analyzer.analyze_template(&root);
        }

        let analysis = analyzer.finish();

        Ok(self.generate(&descriptor, &analysis))
    }
}

impl Default for VirtualTsGenerator {
    fn default() -> Self {
        Self
    }
}

#[cfg(test)]
mod tests {
    use super::VirtualTsGenerator;

    #[test]
    fn test_generate_from_content() {
        let generator = VirtualTsGenerator::new();
        let content = r#"<template>
  <div>{{ message }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Child from './Child.vue'

const message = ref('Hello')
</script>
"#;

        let result = generator.generate_from_content(content);
        assert!(result.is_ok());

        let result = result.unwrap();
        // Should have .vue.ts import
        assert!(result.code.contains("./Child.vue.ts"));
        // Should have compiler macros
        assert!(result.code.contains("function defineProps"));
        // Should have template scope
        assert!(result.code.contains("function __template()"));
    }

    #[test]
    fn test_rewrite_import_line() {
        let rewriter = super::super::import_rewriter::ImportRewriter::new();

        let result = rewriter.rewrite("import App from './App.vue'", oxc_span::SourceType::ts());
        assert_eq!(result.code, "import App from './App.vue.ts'");

        let result = rewriter.rewrite("import { ref } from 'vue'", oxc_span::SourceType::ts());
        assert_eq!(result.code, "import { ref } from 'vue'");
    }
}