Skip to main content

fresh_plugin_api_macros/
lib.rs

1//! # Fresh Plugin API Macros
2//!
3//! Proc macros for generating TypeScript definitions from Rust QuickJS API implementations.
4//!
5//! ## Overview
6//!
7//! This crate provides the `#[plugin_api_impl]` attribute macro that:
8//! 1. Parses method signatures from a `JsEditorApi` impl block
9//! 2. Generates TypeScript type definitions (`.d.ts`)
10//! 3. Automatically writes to `plugins/lib/fresh.d.ts` during compilation
11//!
12//! ## Usage
13//!
14//! ```rust,ignore
15//! use fresh_plugin_api_macros::{plugin_api, plugin_api_impl};
16//!
17//! #[plugin_api_impl]
18//! #[rquickjs::methods(rename_all = "camelCase")]
19//! impl JsEditorApi {
20//!     /// Get the active buffer ID (0 if none)
21//!     pub fn get_active_buffer_id(&self) -> u32 { ... }
22//!
23//!     /// Create a virtual buffer (async)
24//!     #[plugin_api(async_promise, js_name = "createVirtualBuffer", ts_return = "number")]
25//!     #[qjs(rename = "_createVirtualBufferStart")]
26//!     pub fn create_virtual_buffer_start(&self, opts: Object) -> u64 { ... }
27//! }
28//! ```
29//!
30//! ## Attributes
31//!
32//! ### `#[plugin_api_impl]`
33//!
34//! Apply to the impl block to enable TypeScript generation. Generates:
35//! - `{IMPL_NAME}_TYPESCRIPT_DEFINITIONS: &str` - Full `.d.ts` content
36//! - `{IMPL_NAME}_JS_METHODS: &[&str]` - List of all JS method names
37//!
38//! ### `#[plugin_api(...)]`
39//!
40//! Apply to individual methods for customization:
41//!
42//! | Attribute | Description | Example |
43//! |-----------|-------------|---------|
44//! | `skip` | Exclude from TypeScript | `#[plugin_api(skip)]` |
45//! | `js_name = "..."` | Custom JS method name | `#[plugin_api(js_name = "myMethod")]` |
46//! | `async_promise` | Returns `Promise<T>` | `#[plugin_api(async_promise)]` |
47//! | `async_thenable` | Returns `ProcessHandle<T>` (cancellable) | `#[plugin_api(async_thenable)]` |
48//! | `ts_type = "..."` | Custom TypeScript type for parameter | `#[plugin_api(ts_type = "BufferInfo")]` |
49//! | `ts_return = "..."` | Custom TypeScript return type | `#[plugin_api(ts_return = "string")]` |
50//!
51//! ## Type Mapping
52//!
53//! | Rust Type | TypeScript Type | Notes |
54//! |-----------|-----------------|-------|
55//! | `u8`, `u16`, `u32`, `i32`, etc. | `number` | All numeric types |
56//! | `bool` | `boolean` | |
57//! | `String`, `&str` | `string` | |
58//! | `()` | `void` | |
59//! | `Option<T>` | `T \| null` | |
60//! | `Vec<T>` | `T[]` | |
61//! | `rquickjs::Ctx<'js>` | *(skipped)* | Runtime context |
62//! | `rquickjs::function::Opt<T>` | `T?` | Optional parameter |
63//! | `rquickjs::function::Rest<T>` | `...T[]` | Variadic parameter |
64//! | `rquickjs::Result<T>` | `T` | Unwrapped |
65//! | `rquickjs::Object<'js>` | `Record<string, unknown>` | Use `ts_type` for specifics |
66//!
67//! ## Async Methods
68//!
69//! Async methods must be explicitly marked with `#[plugin_api(async_promise)]` or
70//! `#[plugin_api(async_thenable)]`. There is no heuristic-based detection.
71//!
72//! - `async_promise`: For operations that complete with a result
73//! - `async_thenable`: For cancellable operations (e.g., process spawning)
74//!
75//! ## File Output
76//!
77//! The macro automatically writes `plugins/lib/fresh.d.ts` when:
78//! 1. Building the main crate (not the macro crate)
79//! 2. The content has changed (avoids unnecessary rebuilds)
80//!
81//! ## Design Principles
82//!
83//! 1. **Single Source of Truth**: API defined once in Rust, TypeScript generated
84//! 2. **Explicit Over Implicit**: No magic naming conventions, use attributes
85//! 3. **Deterministic Output**: Same input always produces same output
86//! 4. **Preserve Original Code**: Macro passes through impl block unchanged
87//! 5. **Clear Errors**: Compile-time errors with helpful messages
88
89use proc_macro::TokenStream;
90use quote::{format_ident, quote};
91use syn::{
92    parse_macro_input, spanned::Spanned, Attribute, FnArg, GenericArgument, ImplItem, ImplItemFn,
93    ItemImpl, Meta, Pat, PathArguments, ReturnType, Type,
94};
95
96// ============================================================================
97// Error Handling
98// ============================================================================
99
100/// Create a compile error with a helpful message and source span
101fn compile_error(span: proc_macro2::Span, message: &str) -> proc_macro2::TokenStream {
102    syn::Error::new(span, message).to_compile_error()
103}
104
105// ============================================================================
106// API Method Classification
107// ============================================================================
108
109/// Classification of API method return behavior
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111enum ApiKind {
112    /// Synchronous method - returns value directly
113    Sync,
114    /// Async method returning `Promise<T>`
115    AsyncPromise,
116    /// Async method returning `ProcessHandle<T>` (cancellable)
117    AsyncThenable,
118}
119
120impl ApiKind {
121    /// Wrap a TypeScript type in the appropriate async wrapper
122    fn wrap_return_type(&self, inner: &str) -> String {
123        match self {
124            ApiKind::Sync => inner.to_string(),
125            ApiKind::AsyncPromise => format!("Promise<{}>", inner),
126            ApiKind::AsyncThenable => format!("ProcessHandle<{}>", inner),
127        }
128    }
129}
130
131// ============================================================================
132// Parsed Structures (Intermediate Representation)
133// ============================================================================
134
135/// Parsed API method - intermediate representation for code generation
136#[derive(Debug)]
137struct ApiMethod {
138    /// JavaScript method name (camelCase)
139    js_name: String,
140    /// Method classification (sync/async)
141    kind: ApiKind,
142    /// Parsed parameters
143    params: Vec<ParamInfo>,
144    /// TypeScript return type
145    return_type: String,
146    /// Documentation from doc comments
147    doc: String,
148}
149
150/// Parsed parameter information
151#[derive(Debug)]
152struct ParamInfo {
153    /// Parameter name (camelCase)
154    name: String,
155    /// TypeScript type
156    ts_type: String,
157    /// Whether parameter is optional (from `Opt<T>`)
158    optional: bool,
159    /// Whether parameter is variadic (from `Rest<T>`)
160    variadic: bool,
161}
162
163impl ParamInfo {
164    /// Format as TypeScript parameter
165    fn to_typescript(&self) -> String {
166        if self.variadic {
167            format!("...{}: {}[]", self.name, self.ts_type)
168        } else if self.optional {
169            format!("{}?: {}", self.name, self.ts_type)
170        } else {
171            format!("{}: {}", self.name, self.ts_type)
172        }
173    }
174}
175
176// ============================================================================
177// String Utilities
178// ============================================================================
179
180/// Convert snake_case identifier to camelCase
181///
182/// # Examples
183/// ```ignore
184/// assert_eq!(to_camel_case("get_active_buffer"), "getActiveBuffer");
185/// assert_eq!(to_camel_case("simple"), "simple");
186/// ```
187fn to_camel_case(s: &str) -> String {
188    let mut result = String::with_capacity(s.len());
189    let mut capitalize_next = false;
190
191    for c in s.chars() {
192        if c == '_' {
193            capitalize_next = true;
194        } else if capitalize_next {
195            result.push(c.to_ascii_uppercase());
196            capitalize_next = false;
197        } else {
198            result.push(c);
199        }
200    }
201    result
202}
203
204// ============================================================================
205// Attribute Parsing
206// ============================================================================
207
208/// Extract documentation from `#[doc = "..."]` attributes
209fn extract_doc_comment(attrs: &[Attribute]) -> String {
210    attrs
211        .iter()
212        .filter_map(|attr| {
213            if !attr.path().is_ident("doc") {
214                return None;
215            }
216            if let Meta::NameValue(meta) = &attr.meta {
217                if let syn::Expr::Lit(expr_lit) = &meta.value {
218                    if let syn::Lit::Str(lit_str) = &expr_lit.lit {
219                        return Some(lit_str.value().trim().to_string());
220                    }
221                }
222            }
223            None
224        })
225        .collect::<Vec<_>>()
226        .join("\n")
227}
228
229/// Parse a string value from attribute tokens like `key = "value"`
230fn parse_attr_string_value(tokens: &str, key: &str) -> Option<String> {
231    let start = tokens.find(key)?;
232    let rest = &tokens[start..];
233    let eq_pos = rest.find('=')?;
234    let after_eq = rest[eq_pos + 1..].trim();
235
236    if !after_eq.starts_with('"') {
237        return None;
238    }
239
240    let end_quote = after_eq[1..].find('"')?;
241    Some(after_eq[1..end_quote + 1].to_string())
242}
243
244/// Check if `#[plugin_api(...)]` contains a specific flag
245fn has_plugin_api_flag(attrs: &[Attribute], flag: &str) -> bool {
246    attrs.iter().any(|attr| {
247        if !attr.path().is_ident("plugin_api") {
248            return false;
249        }
250        if let Meta::List(meta_list) = &attr.meta {
251            meta_list.tokens.to_string().contains(flag)
252        } else {
253            false
254        }
255    })
256}
257
258/// Get a string value from `#[plugin_api(key = "value")]`
259fn get_plugin_api_value(attrs: &[Attribute], key: &str) -> Option<String> {
260    for attr in attrs {
261        if !attr.path().is_ident("plugin_api") {
262            continue;
263        }
264        if let Meta::List(meta_list) = &attr.meta {
265            if let Some(value) = parse_attr_string_value(&meta_list.tokens.to_string(), key) {
266                return Some(value);
267            }
268        }
269    }
270    None
271}
272
273/// Get custom JS name from `#[qjs(rename = "...")]` or `#[plugin_api(js_name = "...")]`
274fn get_js_name(attrs: &[Attribute]) -> Option<String> {
275    // First check plugin_api attribute (takes precedence)
276    if let Some(name) = get_plugin_api_value(attrs, "js_name") {
277        return Some(name);
278    }
279
280    // Then check qjs attribute
281    for attr in attrs {
282        if !attr.path().is_ident("qjs") {
283            continue;
284        }
285        if let Meta::List(meta_list) = &attr.meta {
286            if let Some(name) = parse_attr_string_value(&meta_list.tokens.to_string(), "rename") {
287                return Some(name);
288            }
289        }
290    }
291    None
292}
293
294// ============================================================================
295// Type Analysis
296// ============================================================================
297
298/// Extract inner type from generic wrapper like `Option<T>`, `Vec<T>`
299fn extract_inner_type(ty: &Type) -> Option<Type> {
300    if let Type::Path(type_path) = ty {
301        if let Some(segment) = type_path.path.segments.last() {
302            if let PathArguments::AngleBracketed(args) = &segment.arguments {
303                if let Some(GenericArgument::Type(inner)) = args.args.first() {
304                    return Some(inner.clone());
305                }
306            }
307        }
308    }
309    None
310}
311
312/// Get the final segment name from a type path (e.g., "Opt" from "rquickjs::function::Opt")
313fn get_type_name(ty: &Type) -> Option<String> {
314    if let Type::Path(type_path) = ty {
315        type_path.path.segments.last().map(|s| s.ident.to_string())
316    } else {
317        None
318    }
319}
320
321/// Check if type is QuickJS context (`Ctx<'js>`) - should be skipped from parameters
322fn is_ctx_type(ty: &Type) -> bool {
323    if let Type::Path(type_path) = ty {
324        // Check final segment
325        if let Some(segment) = type_path.path.segments.last() {
326            if segment.ident == "Ctx" {
327                return true;
328            }
329        }
330        // Check full path for "Ctx" anywhere
331        let path_str: String = type_path
332            .path
333            .segments
334            .iter()
335            .map(|s| s.ident.to_string())
336            .collect::<Vec<_>>()
337            .join("::");
338        path_str.contains("Ctx")
339    } else {
340        false
341    }
342}
343
344/// Check if type is `Opt<T>` (optional parameter)
345fn is_opt_type(ty: &Type) -> bool {
346    get_type_name(ty).is_some_and(|n| n == "Opt")
347}
348
349/// Check if type is `Rest<T>` (variadic parameter)
350fn is_rest_type(ty: &Type) -> bool {
351    get_type_name(ty).is_some_and(|n| n == "Rest")
352}
353
354// ============================================================================
355// Rust to TypeScript Type Conversion
356// ============================================================================
357
358/// Convert a Rust type to its TypeScript equivalent
359///
360/// Handles:
361/// - Primitive types (numbers, bool, string)
362/// - Generic wrappers (Option, Vec, Result)
363/// - QuickJS types (Opt, Rest, Object, Value)
364/// - Known API types (BufferInfo, etc.)
365fn rust_to_typescript(ty: &Type, attrs: &[Attribute]) -> String {
366    // Check for explicit ts_type override
367    if let Some(custom) = get_plugin_api_value(attrs, "ts_type") {
368        return custom;
369    }
370
371    match ty {
372        Type::Path(type_path) => {
373            let type_name = type_path
374                .path
375                .segments
376                .last()
377                .map(|s| s.ident.to_string())
378                .unwrap_or_else(|| "unknown".to_string());
379
380            match type_name.as_str() {
381                // Numeric types -> number
382                "u8" | "u16" | "u32" | "u64" | "i8" | "i16" | "i32" | "i64" | "usize" | "isize"
383                | "f32" | "f64" => "number".to_string(),
384
385                // Boolean
386                "bool" => "boolean".to_string(),
387
388                // String types
389                "String" | "str" => "string".to_string(),
390
391                // Unit type
392                "()" => "void".to_string(),
393
394                // Option<T> -> T | null
395                "Option" => {
396                    let inner = extract_inner_type(ty)
397                        .map(|t| rust_to_typescript(&t, &[]))
398                        .unwrap_or_else(|| "unknown".to_string());
399                    format!("{} | null", inner)
400                }
401
402                // Vec<T> -> T[]
403                "Vec" => {
404                    let inner = extract_inner_type(ty)
405                        .map(|t| rust_to_typescript(&t, &[]))
406                        .unwrap_or_else(|| "unknown".to_string());
407                    format!("{}[]", inner)
408                }
409
410                // Opt<T> -> extract inner (optionality handled at param level)
411                "Opt" => extract_inner_type(ty)
412                    .map(|t| rust_to_typescript(&t, &[]))
413                    .unwrap_or_else(|| "unknown".to_string()),
414
415                // Rest<T> -> extract inner (variadic handled at param level)
416                "Rest" => extract_inner_type(ty)
417                    .map(|t| rust_to_typescript(&t, &[]))
418                    .unwrap_or_else(|| "unknown".to_string()),
419
420                // Result<T, E> -> extract T
421                "Result" => extract_inner_type(ty)
422                    .map(|t| rust_to_typescript(&t, &[]))
423                    .unwrap_or_else(|| "unknown".to_string()),
424
425                // QuickJS types
426                "Value" => "unknown".to_string(),
427                "Object" => "Record<string, unknown>".to_string(),
428
429                // Rust collections
430                "HashMap" | "BTreeMap" => "Record<string, unknown>".to_string(),
431
432                // Known API types - pass through unchanged
433                "BufferInfo"
434                | "CursorInfo"
435                | "ViewportInfo"
436                | "SpawnResult"
437                | "BackgroundProcessResult"
438                | "DirEntry"
439                | "FileStat"
440                | "CreateVirtualBufferResult"
441                | "PromptSuggestion"
442                | "TextPropertyEntry"
443                | "JsTextPropertyEntry"
444                | "CreateVirtualBufferOptions"
445                | "CreateVirtualBufferInSplitOptions"
446                | "CreateVirtualBufferInExistingSplitOptions"
447                | "VirtualBufferResult"
448                | "ActionSpec"
449                | "ActionPopupAction"
450                | "ActionPopupOptions"
451                | "ViewTokenWire"
452                | "ViewTokenStyle"
453                | "LayoutHints"
454                | "FileExplorerDecoration"
455                | "TsCompositeLayoutConfig"
456                | "TsCompositeSourceConfig"
457                | "TsCompositePaneStyle"
458                | "TsHighlightSpan"
459                | "TsActionPopupAction"
460                | "JsDiagnostic"
461                | "CreateTerminalOptions"
462                | "TerminalResult" => type_name,
463
464                // Types renamed by ts-rs — map Rust name to TypeScript name
465                "CompositeHunk" => "TsCompositeHunk".to_string(),
466                "CreateCompositeBufferOptions" => "TsCreateCompositeBufferOptions".to_string(),
467                "Suggestion" => "PromptSuggestion".to_string(),
468
469                // Default: use type name as-is
470                _ => type_name,
471            }
472        }
473        Type::Tuple(tuple) if tuple.elems.is_empty() => "void".to_string(),
474        Type::Reference(reference) => rust_to_typescript(&reference.elem, attrs),
475        _ => "unknown".to_string(),
476    }
477}
478
479// ============================================================================
480// Method Parsing
481// ============================================================================
482
483/// Parse a method from the impl block into an ApiMethod
484///
485/// Returns `None` if the method should be skipped (marked with `skip` or internal)
486fn parse_method(method: &ImplItemFn) -> Option<ApiMethod> {
487    // Skip methods marked with #[plugin_api(skip)]
488    if has_plugin_api_flag(&method.attrs, "skip") {
489        return None;
490    }
491
492    let rust_name = method.sig.ident.to_string();
493    let doc = extract_doc_comment(&method.attrs);
494
495    // Determine method kind from explicit attributes only (no heuristics)
496    let kind = if has_plugin_api_flag(&method.attrs, "async_thenable") {
497        ApiKind::AsyncThenable
498    } else if has_plugin_api_flag(&method.attrs, "async_promise") {
499        ApiKind::AsyncPromise
500    } else {
501        ApiKind::Sync
502    };
503
504    // Get JS name: explicit > snake_to_camel conversion
505    let js_name = get_js_name(&method.attrs).unwrap_or_else(|| to_camel_case(&rust_name));
506
507    // Skip internal methods (names starting with underscore)
508    if js_name.starts_with('_') {
509        return None;
510    }
511
512    // Parse parameters
513    let params: Vec<ParamInfo> = method
514        .sig
515        .inputs
516        .iter()
517        .filter_map(|arg| {
518            let FnArg::Typed(pat_type) = arg else {
519                return None;
520            };
521            let Pat::Ident(pat_ident) = &*pat_type.pat else {
522                return None;
523            };
524
525            let raw_name = pat_ident.ident.to_string();
526
527            // Skip self parameter
528            if raw_name == "self" {
529                return None;
530            }
531
532            // Strip leading underscore (Rust convention for "unused" params)
533            let param_name = raw_name.strip_prefix('_').unwrap_or(&raw_name);
534
535            let ty = &*pat_type.ty;
536
537            // Skip QuickJS context parameter
538            if is_ctx_type(ty) {
539                return None;
540            }
541
542            Some(ParamInfo {
543                name: to_camel_case(param_name),
544                ts_type: rust_to_typescript(ty, &pat_type.attrs),
545                optional: is_opt_type(ty),
546                variadic: is_rest_type(ty),
547            })
548        })
549        .collect();
550
551    // Parse return type
552    let return_type = match &method.sig.output {
553        ReturnType::Default => "void".to_string(),
554        ReturnType::Type(_, ty) => {
555            // Check for explicit ts_return override
556            get_plugin_api_value(&method.attrs, "ts_return")
557                .unwrap_or_else(|| rust_to_typescript(ty, &method.attrs))
558        }
559    };
560
561    Some(ApiMethod {
562        js_name,
563        kind,
564        params,
565        return_type,
566        doc,
567    })
568}
569
570// ============================================================================
571// TypeScript Code Generation
572// ============================================================================
573
574/// Generate TypeScript method signature with JSDoc
575fn generate_ts_method(method: &ApiMethod) -> String {
576    let mut lines = Vec::new();
577
578    // JSDoc comment
579    if !method.doc.is_empty() {
580        lines.push("  /**".to_string());
581        for line in method.doc.lines() {
582            lines.push(format!("   * {}", line));
583        }
584        lines.push("   */".to_string());
585    }
586
587    // Method signature
588    let params: String = method
589        .params
590        .iter()
591        .map(ParamInfo::to_typescript)
592        .collect::<Vec<_>>()
593        .join(", ");
594
595    let return_type = method.kind.wrap_return_type(&method.return_type);
596
597    lines.push(format!(
598        "  {}({}): {};",
599        method.js_name, params, return_type
600    ));
601
602    lines.join("\n")
603}
604
605/// Generate the TypeScript preamble (header comment and getEditor declaration)
606fn generate_ts_preamble() -> &'static str {
607    r#"/**
608 * Fresh Editor TypeScript Plugin API
609 *
610 * This file provides type definitions for the Fresh editor's TypeScript plugin system.
611 * Plugins have access to the global `editor` object which provides methods to:
612 * - Query editor state (buffers, cursors, viewports)
613 * - Modify buffer content (insert, delete text)
614 * - Add visual decorations (overlays, highlighting)
615 * - Interact with the editor UI (status messages, prompts)
616 *
617 * AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
618 * Generated by fresh-plugin-api-macros + ts-rs from JsEditorApi impl
619 */
620
621/**
622 * Get the editor API instance.
623 * Plugins must call this at the top of their file to get a scoped editor object.
624 */
625declare function getEditor(): EditorAPI;
626
627/**
628 * Register a function as a named handler on the global scope.
629 *
630 * Handler functions registered this way can be referenced by name in
631 * `editor.registerCommand()`, `editor.on()`, and mode keybindings.
632 *
633 * The `fn` parameter is typed as `Function` because the runtime passes
634 * different argument shapes depending on the caller: command handlers
635 * receive no arguments, event handlers receive an event-specific data
636 * object (e.g. `{ buffer_id: number }`), and prompt handlers receive
637 * `{ prompt_type: string, input: string }`. Type-annotate your handler
638 * parameters to match the event you are handling.
639 *
640 * @param name - Handler name (referenced by registerCommand, on, etc.)
641 * @param fn - The handler function
642 */
643declare function registerHandler(name: string, fn: Function): void;
644
645/** Handle for a cancellable async operation */
646interface ProcessHandle<T> extends PromiseLike<T> {
647  /** Promise that resolves to the result when complete */
648  readonly result: Promise<T>;
649  /** Cancel/kill the operation. Returns true if cancelled, false if already completed */
650  kill(): Promise<boolean>;
651}
652
653/** Buffer identifier */
654type BufferId = number;
655
656/** Split identifier */
657type SplitId = number;
658
659"#
660}
661
662/// Generate the EditorAPI interface (methods only)
663/// Types are provided separately via ts-rs
664fn generate_editor_api_interface(methods: &[ApiMethod]) -> String {
665    let method_sigs: Vec<String> = methods.iter().map(generate_ts_method).collect();
666
667    format!(
668        "/**\n * Main editor API interface\n */\ninterface EditorAPI {{\n{}\n}}\n",
669        method_sigs.join("\n\n")
670    )
671}
672
673/// Built-in TypeScript types that don't need to be collected
674const BUILTIN_TS_TYPES: &[&str] = &[
675    "number",
676    "string",
677    "boolean",
678    "void",
679    "unknown",
680    "null",
681    "undefined",
682    "Record",
683    "Array",
684    "Promise",
685    "ProcessHandle",
686    "PromiseLike",
687    "BufferId",
688    "SplitId", // Defined in preamble
689];
690
691/// Extract type names from a TypeScript type string
692///
693/// Handles:
694/// - Simple types: "SpawnResult" -> ["SpawnResult"]
695/// - Generics: "ProcessHandle<SpawnResult>" -> ["SpawnResult"]
696/// - Union: "string | null" -> []
697/// - Arrays: "BufferInfo[]" -> ["BufferInfo"]
698fn extract_type_references(ts_type: &str) -> Vec<String> {
699    let mut types = Vec::new();
700
701    // Remove generic wrappers like ProcessHandle<...>, Promise<...>, Array<...>
702    let mut current = ts_type.to_string();
703
704    // Strip outer generics repeatedly
705    while let Some(start) = current.find('<') {
706        if let Some(end) = current.rfind('>') {
707            let outer = current[..start].trim().to_string();
708            let inner = current[start + 1..end].trim().to_string();
709
710            // But check if outer is a custom type we need
711            if !BUILTIN_TS_TYPES.contains(&outer.as_str()) && !outer.is_empty() {
712                types.push(outer);
713            }
714
715            // Process the inner type
716            current = inner;
717        } else {
718            break;
719        }
720    }
721
722    // Handle union types (split by |)
723    for part in current.split('|') {
724        let part = part.trim();
725
726        // Skip built-in types
727        if BUILTIN_TS_TYPES.contains(&part) {
728            continue;
729        }
730
731        // Handle array types like "BufferInfo[]"
732        let part = part.trim_end_matches("[]");
733
734        // Skip Record<...> and other generics
735        if part.contains('<') || part.contains('>') {
736            continue;
737        }
738
739        // Skip empty or built-in
740        if part.is_empty() || BUILTIN_TS_TYPES.contains(&part) {
741            continue;
742        }
743
744        // This looks like a custom type reference
745        if part.chars().next().is_some_and(|c| c.is_uppercase()) {
746            types.push(part.to_string());
747        }
748    }
749
750    types
751}
752
753/// Collect all type references from methods
754fn collect_referenced_types(methods: &[ApiMethod]) -> Vec<String> {
755    let mut types = std::collections::HashSet::new();
756
757    for method in methods {
758        // Collect from return type
759        for ty in extract_type_references(&method.return_type) {
760            types.insert(ty);
761        }
762
763        // Collect from parameters
764        for param in &method.params {
765            for ty in extract_type_references(&param.ts_type) {
766                types.insert(ty);
767            }
768        }
769    }
770
771    let mut sorted: Vec<String> = types.into_iter().collect();
772    sorted.sort();
773    sorted
774}
775
776// ============================================================================
777// Proc Macros
778// ============================================================================
779
780/// Generate TypeScript definitions from a QuickJS impl block
781///
782/// # Generated Constants
783///
784/// - `{IMPL_NAME}_TS_PREAMBLE: &str` - Header comment + getEditor + ProcessHandle + BufferId/SplitId
785/// - `{IMPL_NAME}_TS_EDITOR_API: &str` - Just the EditorAPI interface with methods
786/// - `{IMPL_NAME}_JS_METHODS: &[&str]` - List of all JS method names
787///
788/// The main crate should combine these with ts-rs generated types to create fresh.d.ts.
789///
790/// # Example
791///
792/// ```rust,ignore
793/// #[plugin_api_impl]
794/// #[rquickjs::methods(rename_all = "camelCase")]
795/// impl JsEditorApi {
796///     /// Get the active buffer ID
797///     pub fn get_active_buffer_id(&self) -> u32 { ... }
798///
799///     /// Spawn a process (cancellable)
800///     #[plugin_api(async_thenable, js_name = "spawnProcess", ts_return = "SpawnResult")]
801///     #[qjs(rename = "_spawnProcessStart")]
802///     pub fn spawn_process_start(&self, cmd: String) -> u64 { ... }
803/// }
804/// ```
805///
806/// # Errors
807///
808/// Compile-time error if applied to non-impl items.
809#[proc_macro_attribute]
810pub fn plugin_api_impl(_attr: TokenStream, item: TokenStream) -> TokenStream {
811    let input = parse_macro_input!(item as ItemImpl);
812
813    // Extract impl target name
814    let impl_name = match &*input.self_ty {
815        Type::Path(type_path) => type_path
816            .path
817            .segments
818            .last()
819            .map(|s| s.ident.to_string())
820            .unwrap_or_else(|| "Unknown".to_string()),
821        _ => {
822            return compile_error(
823                input.self_ty.span(),
824                "plugin_api_impl requires a named type (e.g., `impl JsEditorApi`)",
825            )
826            .into();
827        }
828    };
829
830    // Generate constant names
831    let preamble_const = format_ident!("{}_TS_PREAMBLE", impl_name.to_uppercase());
832    let editor_api_const = format_ident!("{}_TS_EDITOR_API", impl_name.to_uppercase());
833    let methods_const = format_ident!("{}_JS_METHODS", impl_name.to_uppercase());
834
835    // Parse methods into intermediate representation
836    let methods: Vec<ApiMethod> = input
837        .items
838        .iter()
839        .filter_map(|item| {
840            if let ImplItem::Fn(method) = item {
841                parse_method(method)
842            } else {
843                None
844            }
845        })
846        .collect();
847
848    // Generate TypeScript parts
849    let preamble = generate_ts_preamble();
850    let editor_api = generate_editor_api_interface(&methods);
851
852    // Collect JS method names
853    let js_names: Vec<&str> = methods.iter().map(|m| m.js_name.as_str()).collect();
854
855    // Collect referenced types (for ts-rs export)
856    let referenced_types = collect_referenced_types(&methods);
857    let types_const = format_ident!("{}_REFERENCED_TYPES", impl_name.to_uppercase());
858
859    // Strip #[plugin_api(...)] attributes from method parameters before emitting,
860    // since plugin_api is a proc_macro_attribute and can't appear on parameters.
861    // The attribute was already read during parse_method for ts_type overrides.
862    let mut cleaned_input = input.clone();
863    for item in &mut cleaned_input.items {
864        if let ImplItem::Fn(method) = item {
865            for arg in &mut method.sig.inputs {
866                if let FnArg::Typed(pat_type) = arg {
867                    pat_type
868                        .attrs
869                        .retain(|attr| !attr.path().is_ident("plugin_api"));
870                }
871            }
872        }
873    }
874
875    // Generate output: original impl + constants
876    let expanded = quote! {
877        #cleaned_input
878
879        /// TypeScript preamble (header, getEditor, ProcessHandle, BufferId, SplitId)
880        ///
881        /// Combine with ts-rs types and EDITOR_API to create fresh.d.ts
882        pub const #preamble_const: &str = #preamble;
883
884        /// TypeScript EditorAPI interface (methods only)
885        ///
886        /// Combine with preamble and ts-rs types to create fresh.d.ts
887        pub const #editor_api_const: &str = #editor_api;
888
889        /// List of all JavaScript method names exposed in the API
890        ///
891        /// Useful for verification and debugging.
892        pub const #methods_const: &[&str] = &[#(#js_names),*];
893
894        /// List of TypeScript types referenced in method signatures
895        ///
896        /// These types need to be defined (via ts-rs or manually) in fresh.d.ts.
897        /// Use this to automatically collect type definitions.
898        pub const #types_const: &[&str] = &[#(#referenced_types),*];
899    };
900
901    TokenStream::from(expanded)
902}
903
904/// Marker attribute for customizing individual API methods
905///
906/// This attribute is parsed by `#[plugin_api_impl]` but doesn't generate any code itself.
907///
908/// # Options
909///
910/// - `skip` - Exclude method from TypeScript generation
911/// - `js_name = "..."` - Custom JavaScript method name
912/// - `async_promise` - Method returns `Promise<T>`
913/// - `async_thenable` - Method returns `ProcessHandle<T>` (cancellable)
914/// - `ts_type = "..."` - Custom TypeScript type for a parameter
915/// - `ts_return = "..."` - Custom TypeScript return type
916///
917/// # Examples
918///
919/// ```rust,ignore
920/// // Skip internal helper
921/// #[plugin_api(skip)]
922/// fn internal_helper(&self) { ... }
923///
924/// // Async method with custom return type
925/// #[plugin_api(async_promise, js_name = "fetchData", ts_return = "DataResult")]
926/// fn fetch_data_start(&self) -> u64 { ... }
927///
928/// // Cancellable operation
929/// #[plugin_api(async_thenable, js_name = "spawnProcess", ts_return = "SpawnResult")]
930/// fn spawn_process_start(&self, cmd: String) -> u64 { ... }
931/// ```
932#[proc_macro_attribute]
933pub fn plugin_api(_attr: TokenStream, item: TokenStream) -> TokenStream {
934    // Marker attribute - passes through unchanged
935    item
936}
937
938// ============================================================================
939// Unit Tests
940// ============================================================================
941
942#[cfg(test)]
943mod tests {
944    use super::*;
945
946    #[test]
947    fn test_to_camel_case() {
948        assert_eq!(to_camel_case("get_active_buffer"), "getActiveBuffer");
949        assert_eq!(to_camel_case("simple"), "simple");
950        assert_eq!(to_camel_case("a_b_c"), "aBC");
951        // Note: leading underscores in parameter names are stripped by parse_method
952        // before to_camel_case is called, so "process_id" is the input, not "_process_id"
953        assert_eq!(to_camel_case("process_id"), "processId");
954        assert_eq!(to_camel_case("already_camel"), "alreadyCamel");
955        assert_eq!(to_camel_case(""), "");
956        assert_eq!(to_camel_case("_leading"), "Leading");
957        assert_eq!(to_camel_case("trailing_"), "trailing");
958    }
959
960    #[test]
961    fn test_parse_attr_string_value() {
962        assert_eq!(
963            parse_attr_string_value(r#"js_name = "myMethod""#, "js_name"),
964            Some("myMethod".to_string())
965        );
966        assert_eq!(
967            parse_attr_string_value(r#"skip, js_name = "foo""#, "js_name"),
968            Some("foo".to_string())
969        );
970        assert_eq!(parse_attr_string_value(r#"skip"#, "js_name"), None);
971        assert_eq!(parse_attr_string_value(r#"js_name = 123"#, "js_name"), None);
972    }
973
974    #[test]
975    fn test_api_kind_wrap_return_type() {
976        assert_eq!(ApiKind::Sync.wrap_return_type("number"), "number");
977        assert_eq!(
978            ApiKind::AsyncPromise.wrap_return_type("number"),
979            "Promise<number>"
980        );
981        assert_eq!(
982            ApiKind::AsyncThenable.wrap_return_type("SpawnResult"),
983            "ProcessHandle<SpawnResult>"
984        );
985    }
986
987    #[test]
988    fn test_param_info_to_typescript() {
989        let regular = ParamInfo {
990            name: "bufferId".to_string(),
991            ts_type: "number".to_string(),
992            optional: false,
993            variadic: false,
994        };
995        assert_eq!(regular.to_typescript(), "bufferId: number");
996
997        let optional = ParamInfo {
998            name: "line".to_string(),
999            ts_type: "number".to_string(),
1000            optional: true,
1001            variadic: false,
1002        };
1003        assert_eq!(optional.to_typescript(), "line?: number");
1004
1005        let variadic = ParamInfo {
1006            name: "parts".to_string(),
1007            ts_type: "string".to_string(),
1008            optional: false,
1009            variadic: true,
1010        };
1011        assert_eq!(variadic.to_typescript(), "...parts: string[]");
1012    }
1013
1014    #[test]
1015    fn test_generate_ts_preamble_contains_required_declarations() {
1016        let preamble = generate_ts_preamble();
1017
1018        // Check essential declarations
1019        assert!(preamble.contains("declare function getEditor(): EditorAPI"));
1020        assert!(preamble.contains("interface ProcessHandle<T>"));
1021        assert!(preamble.contains("type BufferId = number"));
1022        assert!(preamble.contains("type SplitId = number"));
1023
1024        // Check it's marked as auto-generated
1025        assert!(preamble.contains("AUTO-GENERATED FILE"));
1026    }
1027
1028    #[test]
1029    fn test_extract_type_references() {
1030        // Simple type
1031        assert_eq!(extract_type_references("SpawnResult"), vec!["SpawnResult"]);
1032
1033        // Built-in types return empty
1034        assert!(extract_type_references("number").is_empty());
1035        assert!(extract_type_references("string").is_empty());
1036        assert!(extract_type_references("void").is_empty());
1037
1038        // Generic wrapper - extracts inner type
1039        assert_eq!(
1040            extract_type_references("ProcessHandle<SpawnResult>"),
1041            vec!["SpawnResult"]
1042        );
1043        assert_eq!(
1044            extract_type_references("Promise<BufferInfo>"),
1045            vec!["BufferInfo"]
1046        );
1047
1048        // Union with null
1049        assert_eq!(
1050            extract_type_references("CursorInfo | null"),
1051            vec!["CursorInfo"]
1052        );
1053
1054        // Array type
1055        assert_eq!(extract_type_references("BufferInfo[]"), vec!["BufferInfo"]);
1056
1057        // Built-in generics return empty
1058        assert!(extract_type_references("Record<string, unknown>").is_empty());
1059        assert!(extract_type_references("Promise<void>").is_empty());
1060    }
1061
1062    #[test]
1063    fn test_collect_referenced_types() {
1064        let methods = vec![
1065            ApiMethod {
1066                js_name: "spawnProcess".to_string(),
1067                kind: ApiKind::AsyncThenable,
1068                params: vec![],
1069                return_type: "SpawnResult".to_string(),
1070                doc: "".to_string(),
1071            },
1072            ApiMethod {
1073                js_name: "listBuffers".to_string(),
1074                kind: ApiKind::Sync,
1075                params: vec![],
1076                return_type: "BufferInfo[]".to_string(),
1077                doc: "".to_string(),
1078            },
1079        ];
1080
1081        let types = collect_referenced_types(&methods);
1082        assert!(types.contains(&"SpawnResult".to_string()));
1083        assert!(types.contains(&"BufferInfo".to_string()));
1084    }
1085
1086    #[test]
1087    fn test_generate_ts_method_sync() {
1088        let method = ApiMethod {
1089            js_name: "getActiveBufferId".to_string(),
1090            kind: ApiKind::Sync,
1091            params: vec![],
1092            return_type: "number".to_string(),
1093            doc: "Get the active buffer ID".to_string(),
1094        };
1095
1096        let ts = generate_ts_method(&method);
1097        assert!(ts.contains("getActiveBufferId(): number;"));
1098        assert!(ts.contains("Get the active buffer ID"));
1099    }
1100
1101    #[test]
1102    fn test_generate_ts_method_async_promise() {
1103        let method = ApiMethod {
1104            js_name: "delay".to_string(),
1105            kind: ApiKind::AsyncPromise,
1106            params: vec![ParamInfo {
1107                name: "ms".to_string(),
1108                ts_type: "number".to_string(),
1109                optional: false,
1110                variadic: false,
1111            }],
1112            return_type: "void".to_string(),
1113            doc: "".to_string(),
1114        };
1115
1116        let ts = generate_ts_method(&method);
1117        assert!(ts.contains("delay(ms: number): Promise<void>;"));
1118    }
1119
1120    #[test]
1121    fn test_generate_ts_method_async_thenable() {
1122        let method = ApiMethod {
1123            js_name: "spawnProcess".to_string(),
1124            kind: ApiKind::AsyncThenable,
1125            params: vec![
1126                ParamInfo {
1127                    name: "command".to_string(),
1128                    ts_type: "string".to_string(),
1129                    optional: false,
1130                    variadic: false,
1131                },
1132                ParamInfo {
1133                    name: "args".to_string(),
1134                    ts_type: "string".to_string(),
1135                    optional: false,
1136                    variadic: false,
1137                },
1138            ],
1139            return_type: "SpawnResult".to_string(),
1140            doc: "Spawn a process".to_string(),
1141        };
1142
1143        let ts = generate_ts_method(&method);
1144        assert!(
1145            ts.contains("spawnProcess(command: string, args: string): ProcessHandle<SpawnResult>;")
1146        );
1147    }
1148
1149    // ========================================================================
1150    // Tests for ts-rs renamed type mappings
1151    // ========================================================================
1152
1153    /// Helper to parse a Rust type string into a syn::Type
1154    fn parse_type(s: &str) -> Type {
1155        syn::parse_str::<Type>(s).unwrap()
1156    }
1157
1158    #[test]
1159    fn test_renamed_type_composite_hunk() {
1160        let ty = parse_type("Vec<CompositeHunk>");
1161        let ts = rust_to_typescript(&ty, &[]);
1162        assert_eq!(ts, "TsCompositeHunk[]");
1163    }
1164
1165    #[test]
1166    fn test_renamed_type_create_composite_buffer_options() {
1167        let ty = parse_type("CreateCompositeBufferOptions");
1168        let ts = rust_to_typescript(&ty, &[]);
1169        assert_eq!(ts, "TsCreateCompositeBufferOptions");
1170    }
1171
1172    #[test]
1173    fn test_renamed_type_suggestion() {
1174        let ty = parse_type("Vec<Suggestion>");
1175        let ts = rust_to_typescript(&ty, &[]);
1176        assert_eq!(ts, "PromptSuggestion[]");
1177    }
1178
1179    #[test]
1180    fn test_passthrough_type_terminal_result() {
1181        let ty = parse_type("TerminalResult");
1182        let ts = rust_to_typescript(&ty, &[]);
1183        assert_eq!(ts, "TerminalResult");
1184    }
1185
1186    #[test]
1187    fn test_passthrough_type_create_terminal_options() {
1188        let ty = parse_type("CreateTerminalOptions");
1189        let ts = rust_to_typescript(&ty, &[]);
1190        assert_eq!(ts, "CreateTerminalOptions");
1191    }
1192
1193    #[test]
1194    fn test_passthrough_type_cursor_info() {
1195        let ty = parse_type("CursorInfo");
1196        let ts = rust_to_typescript(&ty, &[]);
1197        assert_eq!(ts, "CursorInfo");
1198    }
1199
1200    #[test]
1201    fn test_option_cursor_info() {
1202        let ty = parse_type("Option<CursorInfo>");
1203        let ts = rust_to_typescript(&ty, &[]);
1204        assert_eq!(ts, "CursorInfo | null");
1205    }
1206
1207    #[test]
1208    fn test_extract_type_references_renamed_types() {
1209        // Renamed types should be extracted by their TS name
1210        assert_eq!(
1211            extract_type_references("TsCompositeHunk[]"),
1212            vec!["TsCompositeHunk"]
1213        );
1214        assert_eq!(
1215            extract_type_references("TsCreateCompositeBufferOptions"),
1216            vec!["TsCreateCompositeBufferOptions"]
1217        );
1218        assert_eq!(
1219            extract_type_references("PromptSuggestion[]"),
1220            vec!["PromptSuggestion"]
1221        );
1222    }
1223
1224    #[test]
1225    fn test_extract_type_references_terminal_types() {
1226        assert_eq!(
1227            extract_type_references("Promise<TerminalResult>"),
1228            vec!["TerminalResult"]
1229        );
1230        assert_eq!(
1231            extract_type_references("CreateTerminalOptions"),
1232            vec!["CreateTerminalOptions"]
1233        );
1234    }
1235
1236    #[test]
1237    fn test_extract_type_references_cursor_types() {
1238        assert_eq!(
1239            extract_type_references("CursorInfo | null"),
1240            vec!["CursorInfo"]
1241        );
1242        assert_eq!(extract_type_references("CursorInfo[]"), vec!["CursorInfo"]);
1243    }
1244
1245    #[test]
1246    fn test_generate_ts_method_with_renamed_param_type() {
1247        let method = ApiMethod {
1248            js_name: "updateCompositeAlignment".to_string(),
1249            kind: ApiKind::Sync,
1250            params: vec![
1251                ParamInfo {
1252                    name: "bufferId".to_string(),
1253                    ts_type: "number".to_string(),
1254                    optional: false,
1255                    variadic: false,
1256                },
1257                ParamInfo {
1258                    name: "hunks".to_string(),
1259                    ts_type: "TsCompositeHunk[]".to_string(),
1260                    optional: false,
1261                    variadic: false,
1262                },
1263            ],
1264            return_type: "boolean".to_string(),
1265            doc: "Update alignment hunks".to_string(),
1266        };
1267
1268        let ts = generate_ts_method(&method);
1269        assert!(ts.contains(
1270            "updateCompositeAlignment(bufferId: number, hunks: TsCompositeHunk[]): boolean;"
1271        ));
1272    }
1273
1274    #[test]
1275    fn test_generate_ts_method_cursor_return_types() {
1276        let method = ApiMethod {
1277            js_name: "getPrimaryCursor".to_string(),
1278            kind: ApiKind::Sync,
1279            params: vec![],
1280            return_type: "CursorInfo | null".to_string(),
1281            doc: "Get primary cursor".to_string(),
1282        };
1283        let ts = generate_ts_method(&method);
1284        assert!(ts.contains("getPrimaryCursor(): CursorInfo | null;"));
1285
1286        let method = ApiMethod {
1287            js_name: "getAllCursors".to_string(),
1288            kind: ApiKind::Sync,
1289            params: vec![],
1290            return_type: "CursorInfo[]".to_string(),
1291            doc: "Get all cursors".to_string(),
1292        };
1293        let ts = generate_ts_method(&method);
1294        assert!(ts.contains("getAllCursors(): CursorInfo[];"));
1295
1296        let method = ApiMethod {
1297            js_name: "getAllCursorPositions".to_string(),
1298            kind: ApiKind::Sync,
1299            params: vec![],
1300            return_type: "number[]".to_string(),
1301            doc: "Get all cursor positions".to_string(),
1302        };
1303        let ts = generate_ts_method(&method);
1304        assert!(ts.contains("getAllCursorPositions(): number[];"));
1305    }
1306
1307    #[test]
1308    fn test_generate_ts_method_terminal() {
1309        let method = ApiMethod {
1310            js_name: "createTerminal".to_string(),
1311            kind: ApiKind::AsyncPromise,
1312            params: vec![ParamInfo {
1313                name: "opts".to_string(),
1314                ts_type: "CreateTerminalOptions".to_string(),
1315                optional: true,
1316                variadic: false,
1317            }],
1318            return_type: "TerminalResult".to_string(),
1319            doc: "Create a terminal".to_string(),
1320        };
1321
1322        let ts = generate_ts_method(&method);
1323        assert!(
1324            ts.contains("createTerminal(opts?: CreateTerminalOptions): Promise<TerminalResult>;")
1325        );
1326    }
1327
1328    #[test]
1329    fn test_collect_referenced_types_includes_renamed() {
1330        let methods = vec![
1331            ApiMethod {
1332                js_name: "updateAlignment".to_string(),
1333                kind: ApiKind::Sync,
1334                params: vec![ParamInfo {
1335                    name: "hunks".to_string(),
1336                    ts_type: "TsCompositeHunk[]".to_string(),
1337                    optional: false,
1338                    variadic: false,
1339                }],
1340                return_type: "boolean".to_string(),
1341                doc: "".to_string(),
1342            },
1343            ApiMethod {
1344                js_name: "setSuggestions".to_string(),
1345                kind: ApiKind::Sync,
1346                params: vec![ParamInfo {
1347                    name: "suggestions".to_string(),
1348                    ts_type: "PromptSuggestion[]".to_string(),
1349                    optional: false,
1350                    variadic: false,
1351                }],
1352                return_type: "boolean".to_string(),
1353                doc: "".to_string(),
1354            },
1355            ApiMethod {
1356                js_name: "getPrimaryCursor".to_string(),
1357                kind: ApiKind::Sync,
1358                params: vec![],
1359                return_type: "CursorInfo | null".to_string(),
1360                doc: "".to_string(),
1361            },
1362            ApiMethod {
1363                js_name: "createTerminal".to_string(),
1364                kind: ApiKind::AsyncPromise,
1365                params: vec![ParamInfo {
1366                    name: "opts".to_string(),
1367                    ts_type: "CreateTerminalOptions".to_string(),
1368                    optional: true,
1369                    variadic: false,
1370                }],
1371                return_type: "TerminalResult".to_string(),
1372                doc: "".to_string(),
1373            },
1374        ];
1375
1376        let types = collect_referenced_types(&methods);
1377        assert!(types.contains(&"TsCompositeHunk".to_string()));
1378        assert!(types.contains(&"PromptSuggestion".to_string()));
1379        assert!(types.contains(&"CursorInfo".to_string()));
1380        assert!(types.contains(&"TerminalResult".to_string()));
1381        assert!(types.contains(&"CreateTerminalOptions".to_string()));
1382    }
1383
1384    #[test]
1385    fn test_all_known_types_are_passthrough_or_renamed() {
1386        // Verify that all known types produce expected output
1387        let passthrough_types = vec![
1388            "BufferInfo",
1389            "CursorInfo",
1390            "ViewportInfo",
1391            "SpawnResult",
1392            "BackgroundProcessResult",
1393            "DirEntry",
1394            "PromptSuggestion",
1395            "ActionSpec",
1396            "ActionPopupOptions",
1397            "VirtualBufferResult",
1398            "TerminalResult",
1399            "CreateTerminalOptions",
1400            "TsHighlightSpan",
1401            "JsDiagnostic",
1402        ];
1403
1404        for type_name in &passthrough_types {
1405            let ty = parse_type(type_name);
1406            let ts = rust_to_typescript(&ty, &[]);
1407            assert_eq!(
1408                &ts, type_name,
1409                "Type {} should pass through unchanged",
1410                type_name
1411            );
1412        }
1413
1414        // Renamed types
1415        let renamed = vec![
1416            ("CompositeHunk", "TsCompositeHunk"),
1417            (
1418                "CreateCompositeBufferOptions",
1419                "TsCreateCompositeBufferOptions",
1420            ),
1421            ("Suggestion", "PromptSuggestion"),
1422        ];
1423
1424        for (rust_name, ts_name) in &renamed {
1425            let ty = parse_type(rust_name);
1426            let ts = rust_to_typescript(&ty, &[]);
1427            assert_eq!(
1428                &ts, ts_name,
1429                "Type {} should be renamed to {}",
1430                rust_name, ts_name
1431            );
1432        }
1433    }
1434}