Skip to main content

macroforge_ts_syn/abi/
patch.rs

1//! Patch-based code modification types.
2//!
3//! This module provides the types used by macros to express code modifications.
4//! Instead of returning complete transformed source code, macros return a list
5//! of [`Patch`] operations that describe insertions, replacements, and deletions.
6//!
7//! ## Why Patches?
8//!
9//! The patch-based approach has several advantages:
10//!
11//! 1. **Composable**: Multiple macros can contribute patches to the same file
12//! 2. **Precise**: Each patch targets a specific span, preserving surrounding code
13//! 3. **Traceable**: Patches can track which macro generated them
14//! 4. **Efficient**: Only changed regions need to be processed
15//!
16//! ## Patch Types
17//!
18//! | Variant | Description |
19//! |---------|-------------|
20//! | [`Patch::Insert`] | Insert code at a position (zero-width span) |
21//! | [`Patch::Replace`] | Replace code in a span with new code |
22//! | [`Patch::Delete`] | Remove code in a span |
23//! | [`Patch::InsertRaw`] | Insert raw text with optional context |
24//! | [`Patch::ReplaceRaw`] | Replace with raw text and context |
25//!
26//! ## Example
27//!
28//! ```rust,no_run
29//! use macroforge_ts_syn::{Patch, PatchCode, SpanIR, MacroResult};
30//!
31//! fn add_method_to_class(body_span: SpanIR) -> MacroResult {
32//!     // Insert a method just before the closing brace
33//!     let insert_point = SpanIR::new(body_span.end - 1, body_span.end - 1);
34//!
35//!     let patch = Patch::Insert {
36//!         at: insert_point,
37//!         code: "toString() { return 'MyClass'; }".into(),
38//!         source_macro: Some("Debug".to_string()),
39//!     };
40//!
41//!     MacroResult {
42//!         runtime_patches: vec![patch],
43//!         ..Default::default()
44//!     }
45//! }
46//! ```
47
48use serde::{Deserialize, Serialize};
49
50use crate::abi::{SpanIR, swc_ast};
51
52/// Specifies where generated code should be inserted relative to the target.
53///
54/// This enum provides structured control over code placement, replacing
55/// the string-based marker system (`/* @macroforge:body */`, etc.).
56///
57/// # Positions
58///
59/// ```text
60/// // ─── Top ─────────────────────────
61/// import { foo } from "./runtime";
62///
63/// // ─── Above ───────────────────────
64/// /** @derive(Debug) */
65/// class User {
66///     // ─── Within ──────────────────
67///     name: string;
68///     debug() { ... }  // <-- inserted here
69/// }
70/// // ─── Below ───────────────────────
71/// User.prototype.toJSON = ...;
72///
73/// // ─── Bottom ──────────────────────
74/// export { User };
75/// ```
76///
77/// # Example
78///
79/// ```rust
80/// use macroforge_ts_syn::InsertPos;
81///
82/// // Generated imports go at the top
83/// let import_pos = InsertPos::Top;
84///
85/// // Generated methods go inside the class
86/// let method_pos = InsertPos::Within;
87///
88/// // Prototype extensions go after the class
89/// let proto_pos = InsertPos::Below;
90/// ```
91#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
92pub enum InsertPos {
93    /// Insert at the top of the file (before all other code).
94    /// Use for imports and module-level setup.
95    Top,
96
97    /// Insert immediately before the target declaration.
98    /// Use for helper functions or type declarations that the target depends on.
99    Above,
100
101    /// Insert inside the target's body (class body, interface body, etc.).
102    /// Use for generated methods, properties, or members.
103    Within,
104
105    /// Insert immediately after the target declaration.
106    /// Use for prototype extensions, companion functions, or related code.
107    #[default]
108    Below,
109
110    /// Insert at the bottom of the file (after all other code).
111    /// Use for exports or cleanup code.
112    Bottom,
113}
114#[cfg(feature = "swc")]
115use swc_core::common::{DUMMY_SP, SyntaxContext};
116
117/// A code modification operation returned by macros.
118///
119/// Patches describe how to transform the original source code. They are
120/// applied in order by the macro host to produce the final output.
121///
122/// # Source Macro Tracking
123///
124/// Patches can optionally track which macro generated them via the
125/// `source_macro` field. This is useful for:
126/// - Debugging macro output
127/// - Source mapping in generated code
128/// - Error attribution
129///
130/// Use [`with_source_macro()`](Self::with_source_macro) to set this field.
131///
132/// # Example
133///
134/// ```rust,no_run
135/// use macroforge_ts_syn::{Patch, SpanIR};
136///
137/// // Insert new code
138/// let insert = Patch::Insert {
139///     at: SpanIR::new(100, 100),  // Zero-width span = insertion point
140///     code: "// generated code".into(),
141///     source_macro: Some("MyMacro".to_string()),
142/// };
143///
144/// // Replace existing code
145/// let replace = Patch::Replace {
146///     span: SpanIR::new(50, 75),  // The span to replace
147///     code: "newCode()".into(),
148///     source_macro: None,
149/// };
150///
151/// // Delete code
152/// let delete = Patch::Delete {
153///     span: SpanIR::new(200, 250),
154/// };
155/// ```
156#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
157pub enum Patch {
158    /// Insert code at a specific position.
159    ///
160    /// The `at` span should typically be zero-width (`start == end`)
161    /// to mark an insertion point. The code is inserted at that position.
162    Insert {
163        /// The position to insert at (use zero-width span for pure insertion).
164        at: SpanIR,
165        /// The code to insert.
166        code: PatchCode,
167        /// Which macro generated this patch (e.g., "Debug", "Clone").
168        #[serde(default)]
169        source_macro: Option<String>,
170    },
171
172    /// Replace code in a span with new code.
173    ///
174    /// The original code in `span` is removed and replaced with `code`.
175    Replace {
176        /// The span of code to replace.
177        span: SpanIR,
178        /// The replacement code.
179        code: PatchCode,
180        /// Which macro generated this patch.
181        #[serde(default)]
182        source_macro: Option<String>,
183    },
184
185    /// Delete code in a span.
186    ///
187    /// The code in `span` is removed entirely.
188    Delete {
189        /// The span of code to delete.
190        span: SpanIR,
191    },
192
193    /// Insert raw text with optional context.
194    ///
195    /// Similar to `Insert`, but takes a raw string and optional context
196    /// for more control over formatting.
197    InsertRaw {
198        /// The position to insert at.
199        at: SpanIR,
200        /// The raw code string to insert.
201        code: String,
202        /// Optional context hint (e.g., "class_body", "module_level").
203        context: Option<String>,
204        /// Which macro generated this patch.
205        #[serde(default)]
206        source_macro: Option<String>,
207    },
208
209    /// Replace code with raw text and optional context.
210    ///
211    /// Similar to `Replace`, but takes a raw string and optional context.
212    ReplaceRaw {
213        /// The span of code to replace.
214        span: SpanIR,
215        /// The raw replacement code string.
216        code: String,
217        /// Optional context hint for formatting.
218        context: Option<String>,
219        /// Which macro generated this patch.
220        #[serde(default)]
221        source_macro: Option<String>,
222    },
223}
224
225impl Patch {
226    /// Get the source macro name for this patch, if set
227    pub fn source_macro(&self) -> Option<&str> {
228        match self {
229            Patch::Insert { source_macro, .. } => source_macro.as_deref(),
230            Patch::Replace { source_macro, .. } => source_macro.as_deref(),
231            Patch::Delete { .. } => None,
232            Patch::InsertRaw { source_macro, .. } => source_macro.as_deref(),
233            Patch::ReplaceRaw { source_macro, .. } => source_macro.as_deref(),
234        }
235    }
236
237    /// Set the source macro name for this patch
238    pub fn with_source_macro(self, macro_name: &str) -> Self {
239        match self {
240            Patch::Insert { at, code, .. } => Patch::Insert {
241                at,
242                code,
243                source_macro: Some(macro_name.to_string()),
244            },
245            Patch::Replace { span, code, .. } => Patch::Replace {
246                span,
247                code,
248                source_macro: Some(macro_name.to_string()),
249            },
250            Patch::Delete { span } => Patch::Delete { span },
251            Patch::InsertRaw {
252                at, code, context, ..
253            } => Patch::InsertRaw {
254                at,
255                code,
256                context,
257                source_macro: Some(macro_name.to_string()),
258            },
259            Patch::ReplaceRaw {
260                span,
261                code,
262                context,
263                ..
264            } => Patch::ReplaceRaw {
265                span,
266                code,
267                context,
268                source_macro: Some(macro_name.to_string()),
269            },
270        }
271    }
272}
273
274/// The code content of a patch, supporting both text and AST representations.
275///
276/// `PatchCode` can hold code in different forms:
277///
278/// - **Text**: Raw source code strings (serializable)
279/// - **AST nodes**: SWC AST types for programmatic construction (not serializable)
280///
281/// When serialized, AST variants are converted to placeholder text since
282/// SWC AST nodes are not directly serializable.
283///
284/// # Example
285///
286/// ```rust
287/// use macroforge_ts_syn::PatchCode;
288///
289/// // From a string
290/// let _text_code: PatchCode = "myMethod() {}".into();
291///
292/// // From a String
293/// let method_code = "toString() { return 'MyClass'; }".to_string();
294/// let _code: PatchCode = method_code.into();
295/// ```
296#[derive(Clone, Debug, PartialEq)]
297pub enum PatchCode {
298    /// Raw source code as text.
299    ///
300    /// This is the most portable form, as it can be serialized
301    /// and processed by any consumer.
302    Text(String),
303
304    /// An SWC class member AST node.
305    ///
306    /// Use this when constructing class members programmatically
307    /// using SWC's AST types or the `quote!` macro.
308    ClassMember(swc_ast::ClassMember),
309
310    /// An SWC statement AST node.
311    ///
312    /// Use for standalone statements or function/method bodies.
313    Stmt(swc_ast::Stmt),
314
315    /// An SWC module item AST node.
316    ///
317    /// Use for top-level declarations like imports, exports,
318    /// or function/class declarations.
319    ModuleItem(swc_ast::ModuleItem),
320}
321
322// Custom serde for PatchCode - only serialize Text variant, skip AST variants
323impl serde::Serialize for PatchCode {
324    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
325    where
326        S: serde::Serializer,
327    {
328        match self {
329            PatchCode::Text(s) => serializer.serialize_str(s),
330            _ => serializer.serialize_str("/* AST node - cannot serialize */"),
331        }
332    }
333}
334
335impl<'de> serde::Deserialize<'de> for PatchCode {
336    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
337    where
338        D: serde::Deserializer<'de>,
339    {
340        let s = String::deserialize(deserializer)?;
341        Ok(PatchCode::Text(s))
342    }
343}
344
345impl From<String> for PatchCode {
346    fn from(value: String) -> Self {
347        PatchCode::Text(value)
348    }
349}
350
351impl From<&str> for PatchCode {
352    fn from(value: &str) -> Self {
353        PatchCode::Text(value.to_string())
354    }
355}
356
357impl From<swc_ast::ClassMember> for PatchCode {
358    fn from(member: swc_ast::ClassMember) -> Self {
359        PatchCode::ClassMember(member)
360    }
361}
362
363impl From<swc_ast::Stmt> for PatchCode {
364    fn from(stmt: swc_ast::Stmt) -> Self {
365        PatchCode::Stmt(stmt)
366    }
367}
368
369impl From<swc_ast::ModuleItem> for PatchCode {
370    fn from(item: swc_ast::ModuleItem) -> Self {
371        PatchCode::ModuleItem(item)
372    }
373}
374
375impl From<Vec<swc_ast::Stmt>> for PatchCode {
376    fn from(stmts: Vec<swc_ast::Stmt>) -> Self {
377        // For Vec<Stmt>, wrap in a block and convert to a single Stmt
378        if stmts.len() == 1 {
379            PatchCode::Stmt(stmts.into_iter().next().unwrap())
380        } else {
381            PatchCode::Stmt(swc_ast::Stmt::Block(swc_ast::BlockStmt {
382                span: DUMMY_SP,
383                ctxt: SyntaxContext::empty(),
384                stmts,
385            }))
386        }
387    }
388}
389
390impl From<Vec<swc_ast::ModuleItem>> for PatchCode {
391    fn from(items: Vec<swc_ast::ModuleItem>) -> Self {
392        // For Vec<ModuleItem>, take the first if there's only one
393        if items.len() == 1 {
394            PatchCode::ModuleItem(items.into_iter().next().unwrap())
395        } else {
396            // Multiple items - convert to a string representation
397            // This is a limitation since PatchCode doesn't have a Vec variant
398            let code = items
399                .iter()
400                .map(|_| "/* generated code */")
401                .collect::<Vec<_>>()
402                .join("\n");
403            PatchCode::Text(code)
404        }
405    }
406}
407
408/// The result returned by a macro function.
409///
410/// `MacroResult` is the standard return type for all macro functions.
411/// It contains patches to apply to the code, diagnostic messages,
412/// and optional debug information.
413///
414/// # Patch Separation
415///
416/// Patches are separated into two categories:
417///
418/// - `runtime_patches`: Applied to the `.js`/`.ts` runtime code
419/// - `type_patches`: Applied to `.d.ts` type declaration files
420///
421/// This separation allows macros to generate different code for
422/// runtime and type-checking contexts.
423///
424/// # Example
425///
426/// ```rust,no_run
427/// use macroforge_ts_syn::{MacroResult, Patch, Diagnostic, DiagnosticLevel, InsertPos};
428///
429/// fn my_macro() -> MacroResult {
430///     // Success with patches
431///     MacroResult {
432///         runtime_patches: vec![/* ... */],
433///         type_patches: vec![],
434///         diagnostics: vec![],
435///         tokens: None,
436///         insert_pos: InsertPos::Below,
437///         debug: Some("Generated 2 methods".to_string()),
438///         ..Default::default()
439///     }
440/// }
441///
442/// fn my_macro_error() -> MacroResult {
443///     // Error result
444///     MacroResult {
445///         diagnostics: vec![Diagnostic {
446///             level: DiagnosticLevel::Error,
447///             message: "Invalid input".to_string(),
448///             span: None,
449///             notes: vec![],
450///             help: Some("Try using @derive(Debug) instead".to_string()),
451///         }],
452///         ..Default::default()
453///     }
454/// }
455/// ```
456#[derive(Serialize, Deserialize, Clone, Debug, Default)]
457pub struct MacroResult {
458    /// Patches to apply to the runtime JS/TS code.
459    pub runtime_patches: Vec<Patch>,
460
461    /// Patches to apply to the `.d.ts` type declarations.
462    pub type_patches: Vec<Patch>,
463
464    /// Diagnostic messages (errors, warnings, info).
465    /// If any error diagnostics are present, the macro expansion is considered failed.
466    pub diagnostics: Vec<Diagnostic>,
467
468    /// Optional raw token stream (source code) returned by the macro.
469    /// Used for macros that generate complete output rather than patches.
470    pub tokens: Option<String>,
471
472    /// Where to insert the tokens relative to the target.
473    /// Defaults to `Below` (after the target declaration).
474    /// This is used when `tokens` is set and no string markers are present.
475    #[serde(default)]
476    pub insert_pos: InsertPos,
477
478    /// Optional debug information for development.
479    /// Can be displayed in verbose mode or logged for debugging.
480    pub debug: Option<String>,
481
482    /// Cross-module function suffixes registered by external macros for auto-import resolution.
483    /// The framework combines these with its built-in suffixes when resolving cross-module
484    /// references following the `{camelCaseTypeName}{Suffix}` naming convention.
485    #[serde(default)]
486    pub cross_module_suffixes: Vec<String>,
487
488    /// Cross-module type suffixes for auto-import resolution of PascalCase type references.
489    /// Unlike `cross_module_suffixes` (which resolve `{camelCase}{Suffix}` function calls),
490    /// these resolve `{PascalCase}{Suffix}` type references and generate `import type` statements.
491    /// Used for types like `ColorsErrors`, `ColorsTainted`, `ColorsFieldControllers`.
492    #[serde(default)]
493    pub cross_module_type_suffixes: Vec<String>,
494
495    /// Imports requested by the macro via `TsStream::add_import()` and related methods.
496    /// These are captured from the thread-local `ImportRegistry` when `into_result()` is called,
497    /// so they survive serialization across process boundaries (important for external macros
498    /// that run in a child Node.js process).
499    #[serde(default)]
500    pub imports: Vec<crate::import_registry::GeneratedImport>,
501}
502
503/// A diagnostic message from macro expansion.
504///
505/// Diagnostics provide feedback about the macro expansion process,
506/// including errors, warnings, and informational messages. Each
507/// diagnostic can include a source span, additional notes, and
508/// help text.
509///
510/// # Example
511///
512/// ```rust,no_run
513/// use macroforge_ts_syn::{Diagnostic, DiagnosticLevel, SpanIR};
514///
515/// let error = Diagnostic {
516///     level: DiagnosticLevel::Error,
517///     message: "Field 'password' cannot be serialized".to_string(),
518///     span: Some(SpanIR::new(100, 115)),
519///     notes: vec!["Sensitive fields should use @serde(skip)".to_string()],
520///     help: Some("Add @serde(skip) decorator to this field".to_string()),
521/// };
522/// ```
523#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
524pub struct Diagnostic {
525    /// The severity level of the diagnostic.
526    pub level: DiagnosticLevel,
527
528    /// The main diagnostic message.
529    pub message: String,
530
531    /// Optional source span where the issue occurred.
532    /// If `None`, the diagnostic applies to the whole macro invocation.
533    pub span: Option<SpanIR>,
534
535    /// Additional notes providing context.
536    pub notes: Vec<String>,
537
538    /// Optional help text suggesting how to fix the issue.
539    pub help: Option<String>,
540}
541
542/// The severity level of a diagnostic message.
543#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
544pub enum DiagnosticLevel {
545    /// An error that prevents successful macro expansion.
546    /// Any error diagnostic causes the macro result to be considered failed.
547    Error,
548
549    /// A warning that doesn't prevent expansion but indicates potential issues.
550    Warning,
551
552    /// Informational message for debugging or user guidance.
553    Info,
554}
555
556/// A builder for collecting diagnostics during macro expansion.
557///
558/// `DiagnosticCollector` provides a convenient way to accumulate
559/// multiple diagnostics and then convert them to a `Vec<Diagnostic>`
560/// for inclusion in a `MacroResult`.
561///
562/// # Example
563///
564/// ```rust
565/// use macroforge_ts_syn::{DiagnosticCollector, SpanIR};
566///
567/// let mut collector = DiagnosticCollector::new();
568///
569/// // Add diagnostics as you encounter issues
570/// collector.warning(SpanIR::new(10, 20), "Avoid using 'any' type");
571/// collector.warning(SpanIR::new(30, 40), "Consider using strict mode");
572///
573/// // Convert to a vector for MacroResult
574/// let diagnostics = collector.into_vec();
575/// assert_eq!(diagnostics.len(), 2);
576/// ```
577#[derive(Default, Clone, Debug)]
578pub struct DiagnosticCollector {
579    diagnostics: Vec<Diagnostic>,
580}
581
582impl DiagnosticCollector {
583    /// Create a new empty collector
584    pub fn new() -> Self {
585        Self::default()
586    }
587
588    /// Add a diagnostic to the collection
589    pub fn push(&mut self, diagnostic: Diagnostic) {
590        self.diagnostics.push(diagnostic);
591    }
592
593    /// Add an error diagnostic with span
594    pub fn error(&mut self, span: SpanIR, message: impl Into<String>) {
595        self.push(Diagnostic {
596            level: DiagnosticLevel::Error,
597            message: message.into(),
598            span: Some(span),
599            notes: vec![],
600            help: None,
601        });
602    }
603
604    /// Add an error diagnostic with span and help text
605    pub fn error_with_help(
606        &mut self,
607        span: SpanIR,
608        message: impl Into<String>,
609        help: impl Into<String>,
610    ) {
611        self.push(Diagnostic {
612            level: DiagnosticLevel::Error,
613            message: message.into(),
614            span: Some(span),
615            notes: vec![],
616            help: Some(help.into()),
617        });
618    }
619
620    /// Add a warning diagnostic with span
621    pub fn warning(&mut self, span: SpanIR, message: impl Into<String>) {
622        self.push(Diagnostic {
623            level: DiagnosticLevel::Warning,
624            message: message.into(),
625            span: Some(span),
626            notes: vec![],
627            help: None,
628        });
629    }
630
631    /// Merge diagnostics from another collector
632    pub fn extend(&mut self, other: DiagnosticCollector) {
633        self.diagnostics.extend(other.diagnostics);
634    }
635
636    /// Check if there are any errors in the collection
637    pub fn has_errors(&self) -> bool {
638        self.diagnostics
639            .iter()
640            .any(|d| d.level == DiagnosticLevel::Error)
641    }
642
643    /// Check if the collection is empty
644    pub fn is_empty(&self) -> bool {
645        self.diagnostics.is_empty()
646    }
647
648    /// Get the number of diagnostics
649    pub fn len(&self) -> usize {
650        self.diagnostics.len()
651    }
652
653    /// Convert to a Vec of Diagnostics
654    pub fn into_vec(self) -> Vec<Diagnostic> {
655        self.diagnostics
656    }
657
658    /// Get a reference to the diagnostics
659    pub fn diagnostics(&self) -> &[Diagnostic] {
660        &self.diagnostics
661    }
662}