1#![warn(missing_docs)]
59#![allow(clippy::doc_markdown)] pub mod builder;
62pub mod codegen;
63pub mod error;
64pub mod hir;
65pub mod manifest;
66
67pub use error::{JsGenError, Result};
68
69pub mod prelude {
77 pub use crate::builder::{JsClassBuilder, JsModuleBuilder, JsSwitchBuilder};
78 pub use crate::codegen::generate;
79 pub use crate::error::{JsGenError, Result};
80 pub use crate::hir::{
81 BinOp, Expr, GenerationMetadata, Identifier, JsClass, JsMethod, JsModule, JsSwitch, Stmt,
82 UnaryOp,
83 };
84 pub use crate::manifest::{verify, write_with_manifest, FileManifest};
85}
86
87pub mod validator {
91 pub const FORBIDDEN_PATTERNS: &[&str] = &[
93 "window.", "window)", "window,", "document.", "importScripts", "eval(", "Function(", "with(", "__proto__", ];
103
104 pub const REQUIRED_WORKER_PATTERNS: &[&str] = &[
106 "self.", "import(", ];
109
110 pub fn validate_worker_js(js: &str) -> Vec<String> {
116 let mut errors = Vec::new();
117
118 for pattern in FORBIDDEN_PATTERNS {
120 if js.contains(pattern) {
121 errors.push(format!("Forbidden pattern found: '{pattern}'"));
122 }
123 }
124
125 for pattern in REQUIRED_WORKER_PATTERNS {
127 if !js.contains(pattern) {
128 errors.push(format!("Required pattern missing: '{pattern}'"));
129 }
130 }
131
132 errors
133 }
134
135 pub fn validate_worklet_js(js: &str) -> Vec<String> {
141 let mut errors = Vec::new();
142
143 for pattern in FORBIDDEN_PATTERNS {
145 if js.contains(pattern) {
146 errors.push(format!("Forbidden pattern found: '{pattern}'"));
147 }
148 }
149
150 if !js.contains("extends AudioWorkletProcessor") {
152 errors.push("Missing AudioWorkletProcessor base class".to_string());
153 }
154 if !js.contains("registerProcessor") {
155 errors.push("Missing registerProcessor call".to_string());
156 }
157 if !js.contains("process(") {
158 errors.push("Missing process() method".to_string());
159 }
160
161 errors
162 }
163
164 #[cfg(test)]
165 mod tests {
166 use super::*;
167
168 #[test]
169 fn forbidden_patterns_detected() {
170 let js = "window.alert('test')";
171 let errors = validate_worker_js(js);
172 assert!(!errors.is_empty());
173 assert!(errors[0].contains("window."));
174 }
175
176 #[test]
177 fn valid_worker_js() {
178 let js = r#"
179 self.onmessage = async function(e) {
180 const mod = await import("./module.js");
181 };
182 "#;
183 let errors = validate_worker_js(js);
184 assert!(errors.is_empty(), "Errors: {:?}", errors);
185 }
186
187 #[test]
188 fn valid_worklet_js() {
189 let js = r#"
190 class MyProcessor extends AudioWorkletProcessor {
191 process(inputs, outputs, params) {
192 return true;
193 }
194 }
195 registerProcessor("my-processor", MyProcessor);
196 "#;
197 let errors = validate_worklet_js(js);
199 assert!(errors.is_empty(), "Errors: {:?}", errors);
200 }
201 }
202}
203
204#[cfg(test)]
205#[allow(clippy::unwrap_used)]
206mod integration_tests {
207 use super::prelude::*;
208
209 #[test]
210 fn end_to_end_worker_generation() {
211 let module = JsModuleBuilder::new()
212 .comment("Whisper.apr Transcription Worker (ES Module)")
213 .comment("Generated by probar-js-gen - DO NOT EDIT MANUALLY")
214 .let_decl("wasm", Expr::null())
215 .unwrap()
216 .let_decl("worker", Expr::null())
217 .unwrap()
218 .let_decl("initialized", Expr::bool(false))
219 .unwrap()
220 .stmt(Stmt::on_message(vec![
221 Stmt::const_decl("msg", Expr::ident("e").unwrap().dot("data").unwrap()).unwrap(),
222 Stmt::if_then(
223 Expr::ident("msg")
224 .unwrap()
225 .dot("type")
226 .unwrap()
227 .eq(Expr::str("bootstrap")),
228 vec![Stmt::expr(
229 Expr::ident("console")
230 .unwrap()
231 .dot("log")
232 .unwrap()
233 .call(vec![Expr::str("[Worker] Bootstrap received")]),
234 )],
235 ),
236 ]))
237 .expr(
238 Expr::ident("console")
239 .unwrap()
240 .dot("log")
241 .unwrap()
242 .call(vec![Expr::str(
243 "[Worker] Module loaded, waiting for bootstrap",
244 )]),
245 )
246 .build();
247
248 let js = generate(&module);
249
250 assert!(js.contains("self.onmessage"), "Missing self.onmessage");
252
253 assert!(!js.contains("window"), "Has window (forbidden)");
255 }
256
257 #[test]
258 fn end_to_end_worklet_generation() {
259 let class = JsClassBuilder::new("WhisperProcessor")
260 .unwrap()
261 .extends("AudioWorkletProcessor")
262 .unwrap()
263 .constructor(vec![Stmt::member_assign(
264 Expr::this(),
265 "buffer",
266 Expr::null(),
267 )
268 .unwrap()])
269 .method(
270 "process",
271 &["inputs", "outputs", "params"],
272 vec![Stmt::ret_val(Expr::bool(true))],
273 )
274 .unwrap()
275 .build();
276
277 let module = JsModuleBuilder::new()
278 .comment("WhisperAudioProcessor - AudioWorklet for real-time audio capture")
279 .class(class)
280 .register_processor("whisper-audio-processor", "WhisperProcessor")
281 .unwrap()
282 .build();
283
284 let js = generate(&module);
285
286 assert!(js.contains("extends AudioWorkletProcessor"));
288 assert!(js.contains("super();"));
289 assert!(js.contains("process(inputs, outputs, params)"));
290 assert!(js.contains("return true;"));
291 assert!(js.contains("registerProcessor"));
292
293 assert!(!js.contains("window"));
295 assert!(!js.contains("document"));
296 }
297
298 #[test]
299 fn deterministic_generation() {
300 let build_module = || {
301 JsModuleBuilder::new()
302 .let_decl("x", Expr::num(1))
303 .unwrap()
304 .const_decl("y", Expr::str("test"))
305 .unwrap()
306 .build()
307 };
308
309 let js1 = generate(&build_module());
310 let js2 = generate(&build_module());
311
312 assert_eq!(js1, js2, "Generation must be deterministic");
313 }
314
315 #[test]
316 fn identifier_validation_rejects_reserved() {
317 assert!(Identifier::new("class").is_err());
318 assert!(Identifier::new("function").is_err());
319 assert!(Identifier::new("await").is_err());
320 assert!(Identifier::new("import").is_err());
321 }
322
323 #[test]
324 fn identifier_validation_rejects_invalid() {
325 assert!(Identifier::new("123start").is_err());
326 assert!(Identifier::new("has-dash").is_err());
327 assert!(Identifier::new("").is_err());
328 }
329
330 #[test]
331 fn identifier_validation_accepts_valid() {
332 assert!(Identifier::new("validName").is_ok());
333 assert!(Identifier::new("_private").is_ok());
334 assert!(Identifier::new("$jquery").is_ok());
335 assert!(Identifier::new("name123").is_ok());
336 }
337}