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}