1#[doc(hidden)]
2pub mod ast;
3pub(crate) mod builtin_names;
4#[doc(hidden)]
5pub mod builtins;
6pub(crate) mod color;
7#[doc(hidden)]
8pub mod host;
9#[doc(hidden)]
10pub mod interp;
11#[doc(hidden)]
12pub mod lexer;
13#[doc(hidden)]
14pub mod parser;
15pub(crate) mod resolve;
16#[doc(hidden)]
17pub mod value;
18
19use crate::{Error, ObjectKind};
20use serde::{Deserialize, Serialize};
21
22#[derive(Debug, Clone, PartialEq, Eq, Default)]
24#[non_exhaustive]
25pub struct ModuleSyntax {
26 pub procedures: Vec<String>,
28}
29
30pub fn check_syntax(source: &str) -> Result<ModuleSyntax, Error> {
38 let empty = std::collections::HashSet::new();
39 check_source(source, None, &resolve::Scope::self_contained(&empty))
40}
41
42pub fn check_syntax_partial(source: &str) -> Result<ModuleSyntax, Error> {
54 let empty = std::collections::HashSet::new();
55 check_source(source, None, &resolve::Scope::partial(&empty))
56}
57
58fn check_source(
59 source: &str,
60 module_name: Option<&str>,
61 scope: &resolve::Scope<'_>,
62) -> Result<ModuleSyntax, Error> {
63 let to_err = |e: parser::ParseError| Error::VbaSyntax {
64 message: e.message,
65 module: module_name.map(str::to_string),
66 line: e.pos.line,
67 column: e.pos.col,
68 };
69 let module = parser::parse_module(source).map_err(to_err)?;
70 resolve::check_module(&module, scope).map_err(to_err)?;
71 Ok(ModuleSyntax {
72 procedures: module.procedures().iter().map(|p| p.name.clone()).collect(),
73 })
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78#[non_exhaustive]
79pub struct RunOutcome {
80 pub type_name: String,
82 pub value: Option<String>,
85 pub mutated: bool,
87}
88
89fn parse_args(args: &[&str]) -> Vec<value::Variant> {
90 args.iter()
91 .map(|a| match value::parse_vba_number(a) {
92 Ok(n) if !a.trim().is_empty() => {
93 value::Variant::from_literal(n, a.contains('.') || a.contains(['e', 'E']))
94 }
95 _ => value::Variant::Str((*a).to_string()),
96 })
97 .collect()
98}
99
100fn to_outcome(result: value::Variant, mutated: bool, interp: &interp::Interpreter) -> RunOutcome {
101 RunOutcome {
102 type_name: interp.type_name_of(&result),
103 value: result.to_vba_string().ok(),
104 mutated,
105 }
106}
107
108fn parse_or_error(source: &str, module: Option<&str>) -> Result<ast::Module, Error> {
109 parser::parse_module(source).map_err(|e| Error::VbaSyntax {
110 message: e.message,
111 module: module.map(str::to_string),
112 line: e.pos.line,
113 column: e.pos.col,
114 })
115}
116
117fn to_runtime_error(e: value::VbaError) -> Error {
118 Error::VbaRuntime {
119 message: e.description,
120 number: e.number,
121 }
122}
123
124impl crate::core::WorkbookManager {
125 pub fn run_macro(
127 &mut self,
128 module: Option<&str>,
129 procedure: &str,
130 args: &[&str],
131 ) -> Result<RunOutcome, Error> {
132 let args = parse_args(args);
133
134 let interp = if let Some(project) = &self.vba_project {
135 if let Some(name) = module
136 && project.find_module(name).is_none()
137 {
138 let available = project.modules.iter().map(|m| m.name.clone()).collect();
139 return Err(Error::not_found_among(
140 ObjectKind::VbaModule,
141 name,
142 available,
143 ));
144 }
145 interp::Interpreter::from_project(project, module).map_err(to_runtime_error)?
146 } else {
147 let source = self.macro_source_for(module, procedure)?;
148 let parsed = parse_or_error(&source, module)?;
149 interp::Interpreter::new(parsed)
150 };
151
152 let host = host::Host::new(self).map_err(to_runtime_error)?;
153 let mut interp = interp.with_host(host);
154
155 let result = interp.run(procedure, args);
156 interp.finish();
157 let mutated = interp.mutated();
158 let result = result.map_err(to_runtime_error)?;
159 Ok(to_outcome(result, mutated, &interp))
160 }
161
162 pub fn run_open_events(&mut self) -> Result<RunOutcome, Error> {
164 let interp = if let Some(project) = &self.vba_project {
165 interp::Interpreter::from_project(project, None).map_err(to_runtime_error)?
166 } else {
167 return Err(Error::not_found(
168 ObjectKind::VbaModule,
169 "Workbook_Open or Auto_Open",
170 ));
171 };
172
173 let host = host::Host::new(self).map_err(to_runtime_error)?;
174 let mut interp = interp.with_host(host);
175
176 interp.run_open_events().map_err(to_runtime_error)?;
177 interp.finish();
178 let mutated = interp.mutated();
179 Ok(RunOutcome {
180 type_name: "Empty".to_string(),
181 value: Some(String::new()),
182 mutated,
183 })
184 }
185
186 fn macro_source_for(&self, module: Option<&str>, procedure: &str) -> Result<String, Error> {
187 let project = self
188 .vba_project
189 .as_ref()
190 .ok_or_else(|| Error::not_found(ObjectKind::VbaModule, module.unwrap_or(procedure)))?;
191 let available = || project.modules.iter().map(|m| m.name.clone()).collect();
192 if let Some(name) = module {
193 return project
194 .find_module(name)
195 .map(|m| m.source.clone())
196 .ok_or_else(|| Error::not_found_among(ObjectKind::VbaModule, name, available()));
197 }
198 project
199 .modules
200 .iter()
201 .find(|m| {
202 parser::parse_module(&m.source).is_ok_and(|module| {
203 module
204 .procedures()
205 .iter()
206 .any(|p| p.name.eq_ignore_ascii_case(procedure))
207 })
208 })
209 .map(|m| m.source.clone())
210 .ok_or_else(|| {
211 Error::not_found_among(
212 ObjectKind::VbaModule,
213 format!("a module declaring '{procedure}'"),
214 available(),
215 )
216 })
217 }
218}
219
220pub fn run_macro(source: &str, procedure: &str, args: &[&str]) -> Result<RunOutcome, Error> {
230 let module = parser::parse_module(source).map_err(|e| Error::VbaSyntax {
231 message: e.message,
232 module: None,
233 line: e.pos.line,
234 column: e.pos.col,
235 })?;
236 let mut interp = interp::Interpreter::new(module);
237 let result = interp
238 .run(procedure, parse_args(args))
239 .map_err(to_runtime_error)?;
240
241 Ok(to_outcome(result, false, &interp))
242}
243
244impl VbaModule {
245 pub fn check_syntax(&self) -> Result<ModuleSyntax, Error> {
247 let empty = std::collections::HashSet::new();
248 check_source(
249 &self.source,
250 Some(&self.name),
251 &resolve::Scope::partial(&empty),
252 )
253 }
254}
255
256#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
259pub enum VbaModuleKind {
260 Standard,
262 Class,
265 Document,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
272pub struct VbaModule {
273 pub name: String,
275 pub kind: VbaModuleKind,
277 pub source: String,
279 pub bound_sheet_id: Option<u64>,
281 #[serde(default)]
285 pub prefix_bytes: Vec<u8>,
286 #[serde(default = "default_module_cookie")]
290 pub module_cookie: u16,
291 #[serde(default)]
295 pub cached_compressed_source: Option<Vec<u8>>,
296}
297
298fn default_module_cookie() -> u16 {
299 0xFFFF
300}
301
302impl VbaModule {
303 pub fn is_document(&self) -> bool {
306 self.kind == VbaModuleKind::Document
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312pub struct VbaProject {
313 pub project_id: String,
315 pub modules: Vec<VbaModule>,
317 #[serde(default)]
321 pub raw_donor: Vec<u8>,
322 #[serde(default)]
325 pub seed_prefix_bytes: Vec<u8>,
326 #[serde(default = "default_module_cookie")]
328 pub seed_module_cookie: u16,
329 #[serde(default)]
332 pub protection_lines: Option<String>,
333}
334
335impl VbaProject {
336 pub fn new_empty() -> Self {
338 VbaProject {
339 project_id: new_project_guid(),
340 modules: Vec::new(),
341 raw_donor: crate::core::vba_synth::synthetic_raw_donor(),
342 seed_prefix_bytes: crate::core::vba_synth::synthetic_module_prefix(),
343 seed_module_cookie: default_module_cookie(),
344 protection_lines: None,
345 }
346 }
347
348 pub fn find_module(&self, name: &str) -> Option<&VbaModule> {
350 self.modules
351 .iter()
352 .find(|m| m.name.eq_ignore_ascii_case(name))
353 }
354
355 pub fn find_module_mut(&mut self, name: &str) -> Option<&mut VbaModule> {
357 self.modules
358 .iter_mut()
359 .find(|m| m.name.eq_ignore_ascii_case(name))
360 }
361
362 pub fn module_name_taken(&self, name: &str) -> bool {
365 self.find_module(name).is_some()
366 }
367
368 pub fn check_modules(&self) -> Vec<(String, Result<ModuleSyntax, Error>)> {
370 self.check_modules_scoped(true)
371 }
372
373 pub fn check_modules_partial(&self) -> Vec<(String, Result<ModuleSyntax, Error>)> {
376 self.check_modules_scoped(false)
377 }
378
379 fn check_modules_scoped(&self, complete: bool) -> Vec<(String, Result<ModuleSyntax, Error>)> {
380 let mut declared: std::collections::HashSet<String> = std::collections::HashSet::new();
381 let parsed: Vec<_> = self
382 .modules
383 .iter()
384 .map(|m| (m, parser::parse_module(&m.source).ok()))
385 .collect();
386 for (_, module) in &parsed {
387 if let Some(module) = module {
388 declared.extend(resolve::declared_names(module));
389 }
390 }
391
392 parsed
393 .iter()
394 .map(|(m, _)| {
395 let scope = resolve::Scope {
396 external: &declared,
397 complete_project: complete,
398 };
399 (
400 m.name.clone(),
401 check_source(&m.source, Some(&m.name), &scope),
402 )
403 })
404 .collect()
405 }
406}
407
408fn new_project_guid() -> String {
409 let hi = crate::core::engine::generate_unique_id();
410 let lo = crate::core::engine::generate_unique_id();
411 format!(
412 "{{{:08X}-{:04X}-{:04X}-{:04X}-{:012X}}}",
413 (hi >> 32) as u32,
414 (hi >> 16) as u16,
415 hi as u16,
416 (lo >> 48) as u16,
417 lo & 0xFFFF_FFFF_FFFF,
418 )
419}
420
421pub fn validate_vba_module_name(name: &str) -> Result<(), String> {
425 let trimmed = name.trim();
426 if trimmed.is_empty() {
427 return Err("Module name cannot be empty".to_string());
428 }
429 if trimmed.chars().count() > 31 {
430 return Err(format!(
431 "Module name '{}' exceeds VBA's 31-character limit",
432 name
433 ));
434 }
435 let first = trimmed.chars().next().unwrap();
436 if !first.is_alphabetic() {
437 return Err(format!("Module name '{}' must start with a letter", name));
438 }
439 if !trimmed.chars().all(|c| c.is_alphanumeric() || c == '_') {
440 return Err(format!(
441 "Module name '{}' may only contain letters, digits, and underscores",
442 name
443 ));
444 }
445 Ok(())
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 fn sample_project() -> VbaProject {
453 VbaProject {
454 project_id: "{00000000-0000-0000-0000-000000000000}".to_string(),
455 modules: vec![
456 VbaModule {
457 name: "ThisWorkbook".to_string(),
458 kind: VbaModuleKind::Document,
459 source: "Attribute VB_Name = \"ThisWorkbook\"\r\n".to_string(),
460 bound_sheet_id: None,
461 prefix_bytes: vec![0xAA; 16],
462 module_cookie: 0xFFFF,
463 cached_compressed_source: None,
464 },
465 VbaModule {
466 name: "Module1".to_string(),
467 kind: VbaModuleKind::Standard,
468 source: "Attribute VB_Name = \"Module1\"\r\nSub Foo()\r\nEnd Sub\r\n"
469 .to_string(),
470 bound_sheet_id: None,
471 prefix_bytes: vec![0xBB; 16],
472 module_cookie: 0xFFFF,
473 cached_compressed_source: None,
474 },
475 ],
476 raw_donor: Vec::new(),
477 seed_prefix_bytes: Vec::new(),
478 seed_module_cookie: 0xFFFF,
479 protection_lines: None,
480 }
481 }
482
483 #[test]
484 fn validate_name_rules() {
485 assert!(validate_vba_module_name("Module1").is_ok());
486 assert!(validate_vba_module_name("_Bad").is_err());
487 assert!(validate_vba_module_name("1Bad").is_err());
488 assert!(validate_vba_module_name("").is_err());
489 assert!(validate_vba_module_name("Has Space").is_err());
490 assert!(validate_vba_module_name("Has-Dash").is_err());
491 assert!(validate_vba_module_name(&"A".repeat(32)).is_err());
492 assert!(validate_vba_module_name(&"A".repeat(31)).is_ok());
493 }
494
495 #[test]
496 fn find_module_case_insensitive() {
497 let project = sample_project();
498 assert!(project.find_module("module1").is_some());
499 assert!(project.find_module("MODULE1").is_some());
500 assert!(project.find_module("Module2").is_none());
501 }
502
503 #[test]
504 fn module_name_taken_case_insensitive() {
505 let project = sample_project();
506 assert!(project.module_name_taken("module1"));
507 assert!(!project.module_name_taken("Module2"));
508 }
509
510 fn project_of(sources: &[(&str, &str)]) -> VbaProject {
511 let mut project = sample_project();
512 project.modules = sources
513 .iter()
514 .map(|(name, source)| VbaModule {
515 name: (*name).to_string(),
516 kind: VbaModuleKind::Standard,
517 source: (*source).to_string(),
518 bound_sheet_id: None,
519 prefix_bytes: vec![0xBB; 16],
520 module_cookie: 0xFFFF,
521 cached_compressed_source: None,
522 })
523 .collect();
524 project
525 }
526
527 const CALLER: &str = "Public Sub Caller()\n DoWork 1\nEnd Sub\n";
528 const CALLEE: &str = "Public Sub DoWork(n As Long)\nEnd Sub\n";
529
530 #[test]
531 fn partial_scope_accepts_a_call_into_source_not_supplied() {
532 assert!(check_syntax(CALLER).is_err());
533 assert!(check_syntax_partial(CALLER).is_ok());
534
535 let dup = "Sub Test()\n Dim x As Long\n Dim x As Long\nEnd Sub\n";
536 assert!(check_syntax(dup).is_err());
537 assert!(check_syntax_partial(dup).is_err());
538 }
539
540 #[test]
541 fn check_modules_resolves_across_siblings() {
542 let project = project_of(&[("Module1", CALLER), ("Module2", CALLEE)]);
543 for (name, result) in project.check_modules() {
544 assert!(result.is_ok(), "{name} should be clean: {result:?}");
545 }
546
547 let alone = project_of(&[("Module1", CALLER)]);
548 let results = alone.check_modules();
549 assert_eq!(results.len(), 1);
550 match &results[0].1 {
551 Err(Error::VbaSyntax {
552 message, module, ..
553 }) => {
554 assert!(message.contains("DoWork"), "{message}");
555 assert_eq!(module.as_deref(), Some("Module1"));
556 }
557 other => panic!("expected a syntax error, got {other:?}"),
558 }
559
560 assert!(alone.check_modules_partial()[0].1.is_ok());
561 }
562
563 #[test]
564 fn set_source_leaves_prefix_bytes_untouched() {
565 let mut project = sample_project();
566 let original_prefix = project.find_module("Module1").unwrap().prefix_bytes.clone();
567 project.find_module_mut("Module1").unwrap().source =
568 "Attribute VB_Name = \"Module1\"\r\nSub Bar()\r\nEnd Sub\r\n".to_string();
569 assert_eq!(
570 project.find_module("Module1").unwrap().prefix_bytes,
571 original_prefix
572 );
573 }
574}