wakaru 1.14.0

Public Rust API for the Wakaru JavaScript decompiler
Documentation
//! Debug workflows: parse/print normalization, per-rule tracing, and stable
//! rule metadata.
//!
//! Rule names and stable rule metadata are available through [`rules`]; rule
//! execution and AST mutation are not exposed.

use crate::error::{from_core_driver_error, Error, ErrorKind, Result};
use crate::{RewriteOptions, Source};

#[derive(Debug, Clone, Default)]
pub struct NormalizeOptions {
    rename_bindings: bool,
}

impl NormalizeOptions {
    pub fn rename_bindings(&self) -> bool {
        self.rename_bindings
    }

    pub fn with_rename_bindings(mut self, enabled: bool) -> Self {
        self.rename_bindings = enabled;
        self
    }
}

#[derive(Debug, Clone)]
pub struct TraceOptions {
    start_from: Option<String>,
    stop_after: Option<String>,
    only_changed: bool,
}

impl Default for TraceOptions {
    fn default() -> Self {
        Self {
            start_from: None,
            stop_after: None,
            only_changed: true,
        }
    }
}

impl TraceOptions {
    pub fn start_from(&self) -> Option<&str> {
        self.start_from.as_deref()
    }

    pub fn stop_after(&self) -> Option<&str> {
        self.stop_after.as_deref()
    }

    pub fn only_changed(&self) -> bool {
        self.only_changed
    }

    pub fn with_start_from(mut self, rule: impl Into<String>) -> Self {
        self.start_from = Some(rule.into());
        self
    }

    pub fn with_stop_after(mut self, rule: impl Into<String>) -> Self {
        self.stop_after = Some(rule.into());
        self
    }

    pub fn with_only_changed(mut self, enabled: bool) -> Self {
        self.only_changed = enabled;
        self
    }
}

/// One rule's before/after snapshot from [`trace_rules`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct TraceEvent {
    pub rule: &'static str,
    pub changed: bool,
    pub before: String,
    pub after: String,
}

/// The pipeline stage a rule belongs to, in execution order.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RuleStage {
    Syntax,
    Helpers,
    Structural,
    Complex,
    Modernization,
    Cleanup,
}

/// Stable metadata for one rule registry entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RuleInfo {
    /// Registry id; repeat passes carry a numeric suffix (e.g. `UnIife2`).
    pub id: &'static str,
    pub stage: RuleStage,
    /// Declared ordering dependencies. This is the subset of dependencies
    /// enforced by the registry, not a complete dependency graph.
    pub requires: &'static [&'static str],
}

/// Parse and re-print one source without running any rewrite rules.
pub fn normalize(input: Source, options: NormalizeOptions) -> Result<String> {
    let input = input.into_parts();
    let core_options = wakaru_core::NormalizeOptions {
        rename_bindings: options.rename_bindings,
        filename: input.filename.clone(),
    };
    wakaru_core::normalize(&input.code, &core_options)
        .map_err(|error| Error::new(ErrorKind::Parse, Some(input.filename), error))
}

/// Run the single-file pipeline capturing per-rule before/after snapshots.
pub fn trace_rules(
    input: Source,
    rewrite: RewriteOptions,
    options: TraceOptions,
) -> Result<Vec<TraceEvent>> {
    let input = input.into_parts();
    if input.source_map.is_some() {
        return Err(Error::new(
            ErrorKind::InvalidOptions,
            Some(input.filename),
            anyhow::anyhow!("rule tracing does not accept an input source map"),
        ));
    }
    let core_options = wakaru_core::DecompileOptions {
        filename: input.filename.clone(),
        dce_mode: rewrite.dce().into_core(),
        level: rewrite.level().into_core(),
        ..Default::default()
    };
    let core_trace = wakaru_core::RuleTraceOptions {
        start_from: options.start_from,
        stop_after: options.stop_after,
        only_changed: options.only_changed,
    };
    wakaru_core::trace_rules(&input.code, core_options, core_trace)
        .map(|events| {
            events
                .into_iter()
                .map(|event| TraceEvent {
                    rule: event.rule,
                    changed: event.changed,
                    before: event.before,
                    after: event.after,
                })
                .collect()
        })
        .map_err(|error| {
            let kind = from_core_driver_error(error.kind());
            Error::new(kind, Some(input.filename), error.into_inner())
        })
}

/// Stable metadata for every rule registry entry, in pipeline execution
/// order. This is the authoritative public roster of the rewrite pipeline.
pub fn rules() -> &'static [RuleInfo] {
    static RULES: std::sync::OnceLock<Box<[RuleInfo]>> = std::sync::OnceLock::new();
    RULES.get_or_init(|| {
        wakaru_core::rule_descriptors()
            .iter()
            .map(|descriptor| RuleInfo {
                id: descriptor.id,
                stage: map_rule_stage(descriptor.stage),
                requires: descriptor.requires,
            })
            .collect()
    })
}

fn map_rule_stage(stage: wakaru_core::RuleStage) -> RuleStage {
    match stage {
        wakaru_core::RuleStage::Syntax => RuleStage::Syntax,
        wakaru_core::RuleStage::Helpers => RuleStage::Helpers,
        wakaru_core::RuleStage::Structural => RuleStage::Structural,
        wakaru_core::RuleStage::Complex => RuleStage::Complex,
        wakaru_core::RuleStage::Modernization => RuleStage::Modernization,
        wakaru_core::RuleStage::Cleanup => RuleStage::Cleanup,
    }
}

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

    #[test]
    fn public_rule_metadata_has_no_execution_surface() {
        let rules = rules();
        assert!(!rules.is_empty());
        assert_eq!(rules.len(), wakaru_core::rule_names().len());
        assert_eq!(rules[0].id, wakaru_core::rule_names()[0]);
    }

    #[test]
    fn normalize_uses_the_source_filename_for_typescript() {
        let output = normalize(
            Source::new("input.ts", "const value: number = 1;"),
            NormalizeOptions::default(),
        )
        .expect("TypeScript source should normalize");
        assert!(output.contains("value"));
    }

    #[test]
    fn trace_rejects_unknown_rule_with_typed_options_error() {
        let error = trace_rules(
            Source::new("input.js", "const value = 1;"),
            RewriteOptions::default(),
            TraceOptions::default().with_stop_after("NoSuchRule"),
        )
        .expect_err("unknown rule should fail");

        assert_eq!(error.kind(), ErrorKind::InvalidOptions);
    }

    #[test]
    fn trace_rejects_bundle_with_typed_input_error() {
        let error = trace_rules(
            Source::new(
                "bundle.js",
                "(self.webpackChunkapp = self.webpackChunkapp || []).push([[1], { 1: module => { module.exports = 1; } }]);",
            ),
            RewriteOptions::default(),
            TraceOptions::default(),
        )
        .expect_err("bundle tracing should fail");

        assert_eq!(error.kind(), ErrorKind::InvalidInput);
    }
}