API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Errors and Diagnostics

A macro reports problems as diagnostics: an error fails the macro, while a warning or note is shown alongside its output. Each one points at the source it is about.

Diagnostic

Rust
pub struct Diagnostic {
    pub level: DiagnosticLevel,   // Error, Warning or Info
    pub message: String,
    pub span: Option<SpanIR>,     // the source it is about
    pub notes: Vec<String>,       // extra lines of context
    pub help: Option<String>,     // a suggested fix
}

Point a diagnostic at the narrowest span that explains it: the field with the bad type, not the whole class. For a problem with the macro's use as a whole, input.error_span() is the macro's name inside @derive(...), falling back to the whole decorator.

Failing With One Error

Return a MacroforgeError:

Rust
use macroforge_ts::ts_syn::{Data, MacroforgeError};

match &input.data {
    Data::Class(class) => { /* ... */ }
    _ => {
        return Err(MacroforgeError::new(
            input.error_span(),
            "@derive(Validate) can only be used on classes",
        ));
    }
}

// Not about any one place in the source:
return Err(MacroforgeError::new_global("the macro configuration is missing"));

Reporting Every Problem at Once

When several fields can be wrong, report them all in one run rather than one per build. Collect them with DiagnosticCollector, then fail if any is an error:

Rust
use macroforge_ts::ts_syn::{DiagnosticCollector, MacroforgeErrors};

let mut diagnostics = DiagnosticCollector::new();
for field in class.fields() {
    if field.ts_type.is_empty() {
        diagnostics.error_with_help(
            field.span,
            format!("field \`{}\` has no type", field.name),
            "add a type annotation",
        );
    }
    if field.ts_type == "any" {
        diagnostics.warning(field.span, format!("field \`{}\` is \`any\`", field.name));
    }
}

if diagnostics.has_errors() {
    return Err(MacroforgeErrors::new(diagnostics.into_vec()).into());
}

MacroforgeErrors converts into a MacroforgeError that keeps every diagnostic, warnings included, so the function can keep returning Result<TsStream, MacroforgeError>. A function can also return Result<TsStream, MacroforgeErrors> directly.

DiagnosticCollector methodAdds
error(span, message)An error
error_with_help(span, message, help)An error with a suggested fix
warning(span, message)A warning
push(diagnostic)Any Diagnostic
extend(other)Everything another collector holds

has_errors(), is_empty() and len() inspect it, and into_vec() hands the diagnostics over.

Warnings on Success

When the macro succeeds, report warnings and notes on the stream it returns. With the collector above, after the error check:

Rust
let mut output = ts_template!(Within { /* ... */ });
output.add_diagnostics(diagnostics.into_vec());
Ok(output)

add_diagnostic adds a single one. Streams injected with {$typescript} or combined with merge keep their diagnostics.

Declarative Macro Errors

Declarative macros report their own errors. Tooling that builds on macroforge_ts_syn's declarative parser receives a DeclarativeError, which carries a span, a message, an optional with_help(...) fix and any number of with_note(...) lines.

Tip
Run macroforge expand on a file to see a macro's diagnostics with their positions, without building the whole project.

Next Steps