aprender-test-js-gen 0.35.0

NASA/DO-178B-grade Rust DSL for type-safe JavaScript generation
Documentation
//! NASA/DO-178B-grade Rust DSL for type-safe JavaScript generation.
//!
//! # Overview
//!
//! `probar-js-gen` provides a type-safe DSL for generating JavaScript code
//! with compile-time validation, runtime verification, and immutability
//! enforcement. The generated code is deterministic and verifiable.
//!
//! # Key Features
//!
//! - **Type Safety**: Invalid JS constructs are unrepresentable
//! - **Identifier Validation**: Reserved words and invalid chars are rejected
//! - **Immutability**: Generated files include hash manifests
//! - **Determinism**: Same input always produces same output
//!
//! # Example
//!
//! ```rust,no_run
//! use probar_js_gen::prelude::*;
//!
//! // Build a JavaScript module
//! let module = JsModuleBuilder::new()
//!     .comment("Generated by probar-js-gen")
//!     .let_decl("x", Expr::num(42)).unwrap()
//!     .const_decl("msg", Expr::str("hello")).unwrap()
//!     .build();
//!
//! // Generate JavaScript code
//! let js = generate(&module);
//! assert!(js.contains("let x = 42;"));
//! ```
//!
//! # Safety Model
//!
//! The crate enforces several safety properties:
//!
//! 1. **No Raw JS**: All JavaScript is generated from typed HIR
//! 2. **No Reserved Words**: Identifier::new() rejects `class`, `function`, etc.
//! 3. **No Invalid Chars**: Identifiers are validated at construction
//! 4. **Manifest Verification**: Generated files are hash-verified
//!
//! # References
//!
//! ## Formal Methods
//! - McKeeman, W.M. (1998) "Differential Testing for Software"
//! - Claessen, K. & Hughes, J. (2000) "QuickCheck"
//! - DeMillo, R.A. et al. (1978) "Hints on Test Data Selection"
//!
//! ## JavaScript Semantics
//! - Maffeis et al. (2008) "An Operational Semantics for JavaScript"
//! - Guha et al. (2010) "The Essence of JavaScript"
//! - ECMA-262 (ES2022) Specification
//!
//! ## Software Safety
//! - Leveson, N. (2012) "Engineering a Safer World"
//! - DO-178C (2011) "Software Considerations in Airborne Systems"

#![warn(missing_docs)]
#![allow(clippy::doc_markdown)] // Allow citation names without backticks

pub mod builder;
pub mod codegen;
pub mod error;
pub mod hir;
pub mod manifest;

pub use error::{JsGenError, Result};

/// Prelude module for convenient imports.
///
/// # Example
///
/// ```rust
/// use probar_js_gen::prelude::*;
/// ```
pub mod prelude {
    pub use crate::builder::{JsClassBuilder, JsModuleBuilder, JsSwitchBuilder};
    pub use crate::codegen::generate;
    pub use crate::error::{JsGenError, Result};
    pub use crate::hir::{
        BinOp, Expr, GenerationMetadata, Identifier, JsClass, JsMethod, JsModule, JsSwitch, Stmt,
        UnaryOp,
    };
    pub use crate::manifest::{verify, write_with_manifest, FileManifest};
}

/// Validator for generated JavaScript.
///
/// Checks for patterns that indicate bugs in code generation.
pub mod validator {
    /// Patterns that should never appear in generated code.
    pub const FORBIDDEN_PATTERNS: &[&str] = &[
        "window.",       // Workers have no window
        "window)",       // Same
        "window,",       // Same
        "document.",     // Workers have no document
        "importScripts", // Use dynamic import() instead
        "eval(",         // Security risk
        "Function(",     // Security risk
        "with(",         // Deprecated
        "__proto__",     // Prototype pollution risk
    ];

    /// Patterns that must appear in Worker code.
    pub const REQUIRED_WORKER_PATTERNS: &[&str] = &[
        "self.",   // Worker global
        "import(", // Dynamic import for modules
    ];

    /// Validate generated JavaScript for Worker context.
    ///
    /// # Errors
    ///
    /// Returns list of validation errors found.
    pub fn validate_worker_js(js: &str) -> Vec<String> {
        let mut errors = Vec::new();

        // Check forbidden patterns
        for pattern in FORBIDDEN_PATTERNS {
            if js.contains(pattern) {
                errors.push(format!("Forbidden pattern found: '{pattern}'"));
            }
        }

        // Check required patterns
        for pattern in REQUIRED_WORKER_PATTERNS {
            if !js.contains(pattern) {
                errors.push(format!("Required pattern missing: '{pattern}'"));
            }
        }

        errors
    }

    /// Validate generated JavaScript for AudioWorklet context.
    ///
    /// # Errors
    ///
    /// Returns list of validation errors found.
    pub fn validate_worklet_js(js: &str) -> Vec<String> {
        let mut errors = Vec::new();

        // Check forbidden patterns
        for pattern in FORBIDDEN_PATTERNS {
            if js.contains(pattern) {
                errors.push(format!("Forbidden pattern found: '{pattern}'"));
            }
        }

        // Worklet-specific requirements
        if !js.contains("extends AudioWorkletProcessor") {
            errors.push("Missing AudioWorkletProcessor base class".to_string());
        }
        if !js.contains("registerProcessor") {
            errors.push("Missing registerProcessor call".to_string());
        }
        if !js.contains("process(") {
            errors.push("Missing process() method".to_string());
        }

        errors
    }

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

        #[test]
        fn forbidden_patterns_detected() {
            let js = "window.alert('test')";
            let errors = validate_worker_js(js);
            assert!(!errors.is_empty());
            assert!(errors[0].contains("window."));
        }

        #[test]
        fn valid_worker_js() {
            let js = r#"
                self.onmessage = async function(e) {
                    const mod = await import("./module.js");
                };
            "#;
            let errors = validate_worker_js(js);
            assert!(errors.is_empty(), "Errors: {:?}", errors);
        }

        #[test]
        fn valid_worklet_js() {
            let js = r#"
                class MyProcessor extends AudioWorkletProcessor {
                    process(inputs, outputs, params) {
                        return true;
                    }
                }
                registerProcessor("my-processor", MyProcessor);
            "#;
            // Note: worklet doesn't need self. or import(
            let errors = validate_worklet_js(js);
            assert!(errors.is_empty(), "Errors: {:?}", errors);
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod integration_tests {
    use super::prelude::*;

    #[test]
    fn end_to_end_worker_generation() {
        let module = JsModuleBuilder::new()
            .comment("Whisper.apr Transcription Worker (ES Module)")
            .comment("Generated by probar-js-gen - DO NOT EDIT MANUALLY")
            .let_decl("wasm", Expr::null())
            .unwrap()
            .let_decl("worker", Expr::null())
            .unwrap()
            .let_decl("initialized", Expr::bool(false))
            .unwrap()
            .stmt(Stmt::on_message(vec![
                Stmt::const_decl("msg", Expr::ident("e").unwrap().dot("data").unwrap()).unwrap(),
                Stmt::if_then(
                    Expr::ident("msg")
                        .unwrap()
                        .dot("type")
                        .unwrap()
                        .eq(Expr::str("bootstrap")),
                    vec![Stmt::expr(
                        Expr::ident("console")
                            .unwrap()
                            .dot("log")
                            .unwrap()
                            .call(vec![Expr::str("[Worker] Bootstrap received")]),
                    )],
                ),
            ]))
            .expr(
                Expr::ident("console")
                    .unwrap()
                    .dot("log")
                    .unwrap()
                    .call(vec![Expr::str(
                        "[Worker] Module loaded, waiting for bootstrap",
                    )]),
            )
            .build();

        let js = generate(&module);

        // Must have self (Worker global)
        assert!(js.contains("self.onmessage"), "Missing self.onmessage");

        // Must NOT have window
        assert!(!js.contains("window"), "Has window (forbidden)");
    }

    #[test]
    fn end_to_end_worklet_generation() {
        let class = JsClassBuilder::new("WhisperProcessor")
            .unwrap()
            .extends("AudioWorkletProcessor")
            .unwrap()
            .constructor(vec![Stmt::member_assign(
                Expr::this(),
                "buffer",
                Expr::null(),
            )
            .unwrap()])
            .method(
                "process",
                &["inputs", "outputs", "params"],
                vec![Stmt::ret_val(Expr::bool(true))],
            )
            .unwrap()
            .build();

        let module = JsModuleBuilder::new()
            .comment("WhisperAudioProcessor - AudioWorklet for real-time audio capture")
            .class(class)
            .register_processor("whisper-audio-processor", "WhisperProcessor")
            .unwrap()
            .build();

        let js = generate(&module);

        // Must have required elements
        assert!(js.contains("extends AudioWorkletProcessor"));
        assert!(js.contains("super();"));
        assert!(js.contains("process(inputs, outputs, params)"));
        assert!(js.contains("return true;"));
        assert!(js.contains("registerProcessor"));

        // Must NOT have forbidden elements
        assert!(!js.contains("window"));
        assert!(!js.contains("document"));
    }

    #[test]
    fn deterministic_generation() {
        let build_module = || {
            JsModuleBuilder::new()
                .let_decl("x", Expr::num(1))
                .unwrap()
                .const_decl("y", Expr::str("test"))
                .unwrap()
                .build()
        };

        let js1 = generate(&build_module());
        let js2 = generate(&build_module());

        assert_eq!(js1, js2, "Generation must be deterministic");
    }

    #[test]
    fn identifier_validation_rejects_reserved() {
        assert!(Identifier::new("class").is_err());
        assert!(Identifier::new("function").is_err());
        assert!(Identifier::new("await").is_err());
        assert!(Identifier::new("import").is_err());
    }

    #[test]
    fn identifier_validation_rejects_invalid() {
        assert!(Identifier::new("123start").is_err());
        assert!(Identifier::new("has-dash").is_err());
        assert!(Identifier::new("").is_err());
    }

    #[test]
    fn identifier_validation_accepts_valid() {
        assert!(Identifier::new("validName").is_ok());
        assert!(Identifier::new("_private").is_ok());
        assert!(Identifier::new("$jquery").is_ok());
        assert!(Identifier::new("name123").is_ok());
    }
}