use std::collections::HashSet;
use crate::diagnostics::{SourceLocation, json_string, locate};
use crate::parser::{
self, CaseMatch, Expr, Program, SourceSpan, SpannedStmt, Stmt, WithMember, WithTarget,
};
use crate::vm;
pub struct Diagnostic {
pub severity: &'static str,
pub code: &'static str,
pub kind: &'static str,
pub message: String,
pub location: Option<SourceLocation>,
}
pub fn run_check(source: &str, file: &str, macro_name: Option<&str>) -> Vec<Diagnostic> {
run_check_impl(source, file, macro_name, &HashSet::new())
}
pub fn run_check_in_project(
source: &str,
file: &str,
macro_name: Option<&str>,
other_module_names: &HashSet<String>,
) -> Vec<Diagnostic> {
run_check_impl(source, file, macro_name, other_module_names)
}
fn run_check_impl(
source: &str,
file: &str,
macro_name: Option<&str>,
other_module_names: &HashSet<String>,
) -> Vec<Diagnostic> {
let prog = match parser::parse_with_span(source) {
Ok(prog) => prog,
Err(e) => {
let location = locate(source, file, e.span);
return vec![Diagnostic {
severity: "error",
code: "E2001",
kind: "parse_error",
message: e.message,
location: Some(location),
}];
}
};
let mut diags = Vec::new();
if let Some(name) = macro_name {
let found = prog.subs.iter().any(|s| s.name == name.to_lowercase());
if !found {
diags.push(Diagnostic {
severity: "error",
code: "E1002",
kind: "undefined_sub_or_function",
message: format!("Sub '{}' not found", name),
location: None,
});
}
}
for (reason, span) in &prog.module_diagnostics {
diags.push(Diagnostic {
severity: "info",
code: "I1002",
kind: "unsupported_construct",
message: reason.clone(),
location: Some(locate(source, file, *span)),
});
}
for sub in &prog.subs {
let local_names = local_scope_names(&sub.name, &sub.params, &sub.body);
walk_body(
&sub.body,
&prog,
&local_names,
other_module_names,
source,
file,
&mut diags,
);
}
for func in &prog.funcs {
let local_names = local_scope_names(&func.name, &func.params, &func.body);
walk_body(
&func.body,
&prog,
&local_names,
other_module_names,
source,
file,
&mut diags,
);
}
collect_extra_compile_diagnostics(&prog, source, file, &mut diags);
diags
}
fn local_scope_names(own_name: &str, params: &[String], body: &[SpannedStmt]) -> HashSet<String> {
let mut names: HashSet<String> = params.iter().cloned().collect();
names.insert(own_name.to_string());
collect_declared_names(body, &mut names);
names
}
fn is_resolvable(
name: &str,
prog: &Program,
local_names: &HashSet<String>,
other_module_names: &HashSet<String>,
) -> bool {
local_names.contains(name)
|| prog.subs.iter().any(|s| s.name == name)
|| prog.funcs.iter().any(|f| f.name == name)
|| other_module_names.contains(name)
|| vm::is_known_builtin_function(name)
}
fn collect_declared_names(body: &[SpannedStmt], names: &mut HashSet<String>) {
for s in body {
match &s.stmt {
Stmt::Assignment { var, .. } => {
names.insert(var.clone());
}
Stmt::CellWrite { .. } => {}
Stmt::SetCalcMode(_) => {}
Stmt::SetAppProp { .. } => {}
Stmt::RangeWrite { .. } => {}
Stmt::RangeCopy { .. } => {}
Stmt::RangeObjectCopy { .. } => {}
Stmt::Set { var, .. } => {
names.insert(var.clone());
}
Stmt::RangePaste { .. } => {}
Stmt::SheetRangePaste { .. } => {}
Stmt::SheetProtection { .. } => {}
Stmt::RangeClear { .. } => {}
Stmt::RangeOffsetWrite { .. } => {}
Stmt::RangeDelete { .. } => {}
Stmt::RangeInsert { .. } => {}
Stmt::RowColDelete { .. } => {}
Stmt::RowColInsert { .. } => {}
Stmt::RangeSort { .. } => {}
Stmt::RangeAutoFilter { .. } => {}
Stmt::RangeName { .. } => {}
Stmt::SheetCellWrite { .. } => {}
Stmt::SheetRangeWrite { .. } => {}
Stmt::WithSheet { body, .. } => collect_declared_names(body, names),
Stmt::SheetsAdd => {}
Stmt::SheetsDelete { .. } => {}
Stmt::For { var, body, .. } => {
names.insert(var.clone());
collect_declared_names(body, names);
}
Stmt::ForEach { var, body, .. } => {
names.insert(var.clone());
collect_declared_names(body, names);
}
Stmt::If {
then_body,
else_body,
..
} => {
collect_declared_names(then_body, names);
collect_declared_names(else_body, names);
}
Stmt::DoLoop { body, .. } => collect_declared_names(body, names),
Stmt::SelectCase {
cases, else_body, ..
} => {
for (_, case_body) in cases {
collect_declared_names(case_body, names);
}
collect_declared_names(else_body, names);
}
Stmt::ExitFor | Stmt::ExitDo | Stmt::ExitSub | Stmt::ExitFunction => {}
Stmt::OnError { .. } => {}
Stmt::OnErrorGoTo(_) => {}
Stmt::Label(_) => {}
Stmt::GoTo(_) => {}
Stmt::Resume { .. } => {}
Stmt::CallSub { .. } => {}
Stmt::Dim => {}
Stmt::DimBare { var } => {
names.insert(var.clone());
}
Stmt::DimArray { name, .. } => {
names.insert(name.clone());
}
Stmt::ReDim { name, .. } => {
names.insert(name.clone());
}
Stmt::ArrayWrite { name, .. } => {
names.insert(name.clone());
}
Stmt::Erase { .. } => {}
Stmt::With { target, body } => {
if let WithTarget::Var(var) = target {
names.insert(var.clone());
}
collect_declared_names(body, names);
}
Stmt::WithDot { .. } => {}
Stmt::MsgBox { .. } => {}
Stmt::RecordSet { var, .. } => {
names.insert(var.clone());
}
Stmt::DimRecord { var, .. } => {
names.insert(var.clone());
}
Stmt::DimArrayRecord { name, .. } => {
names.insert(name.clone());
}
Stmt::DimMulti(decls) => {
for d in decls {
match d {
Stmt::DimRecord { var, .. } | Stmt::DimBare { var } => {
names.insert(var.clone());
}
Stmt::DimArray { name, .. } | Stmt::DimArrayRecord { name, .. } => {
names.insert(name.clone());
}
_ => {}
}
}
}
Stmt::RecordSetNested { var, .. } => {
names.insert(var.clone());
}
Stmt::ArrayRecordSet { name, .. } => {
names.insert(name.clone());
}
Stmt::Unsupported { .. } => {}
Stmt::ErrClear => {}
Stmt::ErrRaise { .. } => {}
}
}
}
fn nested_bodies(stmt: &Stmt) -> Vec<&[SpannedStmt]> {
match stmt {
Stmt::WithSheet { body, .. }
| Stmt::For { body, .. }
| Stmt::ForEach { body, .. }
| Stmt::DoLoop { body, .. }
| Stmt::With { body, .. } => vec![body],
Stmt::If {
then_body,
else_body,
..
} => vec![then_body, else_body],
Stmt::SelectCase {
cases, else_body, ..
} => {
let mut bodies: Vec<&[SpannedStmt]> = cases.iter().map(|(_, b)| b.as_slice()).collect();
bodies.push(else_body);
bodies
}
Stmt::Assignment { .. }
| Stmt::CellWrite { .. }
| Stmt::SetCalcMode(_)
| Stmt::SetAppProp { .. }
| Stmt::RangeWrite { .. }
| Stmt::RangeCopy { .. }
| Stmt::RangeObjectCopy { .. }
| Stmt::Set { .. }
| Stmt::RangePaste { .. }
| Stmt::SheetRangePaste { .. }
| Stmt::SheetProtection { .. }
| Stmt::RangeClear { .. }
| Stmt::RangeOffsetWrite { .. }
| Stmt::RangeDelete { .. }
| Stmt::RangeInsert { .. }
| Stmt::RowColDelete { .. }
| Stmt::RowColInsert { .. }
| Stmt::RangeSort { .. }
| Stmt::RangeAutoFilter { .. }
| Stmt::RangeName { .. }
| Stmt::SheetCellWrite { .. }
| Stmt::SheetRangeWrite { .. }
| Stmt::SheetsAdd
| Stmt::SheetsDelete { .. }
| Stmt::ExitFor
| Stmt::ExitDo
| Stmt::ExitSub
| Stmt::ExitFunction
| Stmt::OnError { .. }
| Stmt::OnErrorGoTo(_)
| Stmt::GoTo(_)
| Stmt::Label(_)
| Stmt::Resume { .. }
| Stmt::CallSub { .. }
| Stmt::Dim
| Stmt::DimBare { .. }
| Stmt::DimArray { .. }
| Stmt::ReDim { .. }
| Stmt::ArrayWrite { .. }
| Stmt::Erase { .. }
| Stmt::WithDot { .. }
| Stmt::MsgBox { .. }
| Stmt::RecordSet { .. }
| Stmt::DimRecord { .. }
| Stmt::DimArrayRecord { .. }
| Stmt::DimMulti(_)
| Stmt::RecordSetNested { .. }
| Stmt::ArrayRecordSet { .. }
| Stmt::Unsupported { .. }
| Stmt::ErrClear
| Stmt::ErrRaise { .. } => vec![],
}
}
fn collect_labels(body: &[SpannedStmt], labels: &mut HashSet<String>) {
for s in body {
if let Stmt::Label(name) = &s.stmt {
labels.insert(name.clone());
}
for nested in nested_bodies(&s.stmt) {
collect_labels(nested, labels);
}
}
}
fn collect_func_calls<'a>(expr: &'a Expr, out: &mut Vec<&'a Expr>) {
if let Expr::FuncCall { args, .. } = expr {
out.push(expr);
for a in args {
collect_func_calls(a, out);
}
return;
}
match expr {
Expr::BinOp { lhs, rhs, .. } => {
collect_func_calls(lhs, out);
collect_func_calls(rhs, out);
}
Expr::UnaryMinus(e) | Expr::UnaryNot(e) => collect_func_calls(e, out),
Expr::CellRead { row, col } => {
collect_func_calls(row, out);
collect_func_calls(col, out);
}
Expr::RangeOffsetRead {
row_off, col_off, ..
} => {
collect_func_calls(row_off, out);
collect_func_calls(col_off, out);
}
Expr::CellsFind { what, .. } => collect_func_calls(what, out),
Expr::SheetCellRead { sheet, row, col } => {
collect_func_calls(sheet, out);
collect_func_calls(row, out);
collect_func_calls(col, out);
}
Expr::SheetRangeRead { sheet, .. } => collect_func_calls(sheet, out),
Expr::WorkbookQualifiedSheet { workbook, sheet } => {
collect_func_calls(workbook, out);
collect_func_calls(sheet, out);
}
Expr::CellsEndProp { row, col, .. } => {
collect_func_calls(row, out);
collect_func_calls(col, out);
}
Expr::ArrayRecordGet { indices, .. } => {
for i in indices {
collect_func_calls(i, out);
}
}
_ => {}
}
}
fn resolved_user_proc_arity(
name: &str,
prog: &Program,
local_names: &HashSet<String>,
) -> Option<usize> {
if local_names.contains(name) {
return None;
}
if let Some(s) = prog.subs.iter().find(|s| s.name == name) {
return Some(s.params.len());
}
if let Some(f) = prog.funcs.iter().find(|f| f.name == name) {
return Some(f.params.len());
}
None
}
pub fn compile_check_errors(
prog: &Program,
other_module_names: &HashSet<String>,
) -> Option<(String, SourceSpan)> {
for sub in &prog.subs {
let local_names = local_scope_names(&sub.name, &sub.params, &sub.body);
let mut labels = HashSet::new();
collect_labels(&sub.body, &mut labels);
if let Some(v) = check_body_for_compile_errors(
&sub.body,
prog,
&local_names,
other_module_names,
&labels,
) {
return Some(v);
}
}
for func in &prog.funcs {
let local_names = local_scope_names(&func.name, &func.params, &func.body);
let mut labels = HashSet::new();
collect_labels(&func.body, &mut labels);
if let Some(v) = check_body_for_compile_errors(
&func.body,
prog,
&local_names,
other_module_names,
&labels,
) {
return Some(v);
}
}
None
}
fn check_body_for_compile_errors(
body: &[SpannedStmt],
prog: &Program,
local_names: &HashSet<String>,
other_module_names: &HashSet<String>,
labels: &HashSet<String>,
) -> Option<(String, SourceSpan)> {
for s in body {
match &s.stmt {
Stmt::GoTo(label) if !labels.contains(label) => {
return Some((format!("GoTo: label '{}' not found", label), s.span));
}
Stmt::OnErrorGoTo(label) if !labels.contains(label) => {
return Some((
format!("On Error GoTo: label '{}' not found", label),
s.span,
));
}
Stmt::CallSub { name, args } => {
if !is_resolvable(name, prog, local_names, other_module_names) {
return Some((format!("Sub/Function '{}' not found", name), s.span));
}
if let Some(arity) = resolved_user_proc_arity(name, prog, local_names)
&& args.len() != arity
{
return Some((
format!(
"'{}' expects {} argument(s), got {}",
name,
arity,
args.len()
),
s.span,
));
}
}
_ => {}
}
let mut exprs = Vec::new();
collect_stmt_exprs(&s.stmt, &mut exprs);
for e in exprs {
let mut calls = Vec::new();
collect_func_calls(e, &mut calls);
for call in calls {
let Expr::FuncCall { name, args } = call else {
continue;
};
if !is_resolvable(name, prog, local_names, other_module_names) {
let msg = vm::builtin_call_error(name)
.unwrap_or_else(|| format!("Unknown VBA function: '{}'", name));
return Some((msg, s.span));
}
if let Some(arity) = resolved_user_proc_arity(name, prog, local_names)
&& args.len() != arity
{
return Some((
format!(
"'{}' expects {} argument(s), got {}",
name,
arity,
args.len()
),
s.span,
));
}
}
}
for nested in nested_bodies(&s.stmt) {
if let Some(v) =
check_body_for_compile_errors(nested, prog, local_names, other_module_names, labels)
{
return Some(v);
}
}
}
None
}
fn collect_extra_compile_diagnostics(
prog: &Program,
source: &str,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
for sub in &prog.subs {
let local_names = local_scope_names(&sub.name, &sub.params, &sub.body);
let mut labels = HashSet::new();
collect_labels(&sub.body, &mut labels);
collect_extra_compile_diagnostics_body(
&sub.body,
prog,
&local_names,
&labels,
source,
file,
diags,
);
}
for func in &prog.funcs {
let local_names = local_scope_names(&func.name, &func.params, &func.body);
let mut labels = HashSet::new();
collect_labels(&func.body, &mut labels);
collect_extra_compile_diagnostics_body(
&func.body,
prog,
&local_names,
&labels,
source,
file,
diags,
);
}
}
fn collect_extra_compile_diagnostics_body(
body: &[SpannedStmt],
prog: &Program,
local_names: &HashSet<String>,
labels: &HashSet<String>,
source: &str,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
for s in body {
match &s.stmt {
Stmt::GoTo(label) if !labels.contains(label) => {
diags.push(Diagnostic {
severity: "error",
code: "E1009",
kind: "undefined_label",
message: format!("GoTo: label '{}' not found", label),
location: Some(locate(source, file, s.span)),
});
}
Stmt::OnErrorGoTo(label) if !labels.contains(label) => {
diags.push(Diagnostic {
severity: "error",
code: "E1009",
kind: "undefined_label",
message: format!("On Error GoTo: label '{}' not found", label),
location: Some(locate(source, file, s.span)),
});
}
Stmt::CallSub { name, args } => {
if let Some(arity) = resolved_user_proc_arity(name, prog, local_names)
&& args.len() != arity
{
diags.push(Diagnostic {
severity: "error",
code: "E1008",
kind: "argument_count_mismatch",
message: format!(
"'{}' expects {} argument(s), got {}",
name,
arity,
args.len()
),
location: Some(locate(source, file, s.span)),
});
}
}
_ => {}
}
let mut exprs = Vec::new();
collect_stmt_exprs(&s.stmt, &mut exprs);
for e in exprs {
let mut calls = Vec::new();
collect_func_calls(e, &mut calls);
for call in calls {
let Expr::FuncCall { name, args } = call else {
continue;
};
if let Some(arity) = resolved_user_proc_arity(name, prog, local_names)
&& args.len() != arity
{
diags.push(Diagnostic {
severity: "error",
code: "E1008",
kind: "argument_count_mismatch",
message: format!(
"'{}' expects {} argument(s), got {}",
name,
arity,
args.len()
),
location: Some(locate(source, file, s.span)),
});
}
}
}
for nested in nested_bodies(&s.stmt) {
collect_extra_compile_diagnostics_body(
nested,
prog,
local_names,
labels,
source,
file,
diags,
);
}
}
}
#[allow(clippy::too_many_arguments)]
fn walk_body(
body: &[SpannedStmt],
prog: &Program,
local_names: &HashSet<String>,
other_module_names: &HashSet<String>,
source: &str,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
for s in body {
if let Stmt::MsgBox { .. } = &s.stmt {
diags.push(Diagnostic {
severity: "info",
code: "I1001",
kind: "interactive_call",
message: "MsgBox displays a dialog and blocks headless execution".to_string(),
location: Some(locate(source, file, s.span)),
});
}
if let Stmt::Unsupported { reason } = &s.stmt {
diags.push(Diagnostic {
severity: "info",
code: "I1002",
kind: "unsupported_construct",
message: reason.clone(),
location: Some(locate(source, file, s.span)),
});
}
if let Stmt::CallSub { name, .. } = &s.stmt
&& !is_resolvable(name, prog, local_names, other_module_names)
{
diags.push(Diagnostic {
severity: "error",
code: "E1002",
kind: "undefined_sub_or_function",
message: format!("Sub/Function '{}' not found", name),
location: Some(locate(source, file, s.span)),
});
}
let mut exprs = Vec::new();
collect_stmt_exprs(&s.stmt, &mut exprs);
for e in exprs {
walk_expr(
e,
prog,
local_names,
other_module_names,
s.span,
source,
file,
diags,
);
}
match &s.stmt {
Stmt::WithSheet { body, .. }
| Stmt::For { body, .. }
| Stmt::ForEach { body, .. }
| Stmt::DoLoop { body, .. }
| Stmt::With { body, .. } => walk_body(
body,
prog,
local_names,
other_module_names,
source,
file,
diags,
),
Stmt::If {
then_body,
else_body,
..
} => {
walk_body(
then_body,
prog,
local_names,
other_module_names,
source,
file,
diags,
);
walk_body(
else_body,
prog,
local_names,
other_module_names,
source,
file,
diags,
);
}
Stmt::SelectCase {
cases, else_body, ..
} => {
for (_, case_body) in cases {
walk_body(
case_body,
prog,
local_names,
other_module_names,
source,
file,
diags,
);
}
walk_body(
else_body,
prog,
local_names,
other_module_names,
source,
file,
diags,
);
}
_ => {}
}
}
}
fn collect_stmt_exprs<'a>(stmt: &'a Stmt, out: &mut Vec<&'a Expr>) {
match stmt {
Stmt::Assignment { value, .. } => out.push(value),
Stmt::CellWrite { row, col, value } => {
out.push(row);
out.push(col);
out.push(value);
}
Stmt::SetCalcMode(_) => {}
Stmt::SetAppProp { value, .. } => out.push(value),
Stmt::RangeWrite { value, .. } => out.push(value),
Stmt::RangeCopy { .. } => {}
Stmt::RangeObjectCopy { .. } => {}
Stmt::Set { .. } => {}
Stmt::RangePaste { transpose, .. } => {
if let Some(e) = transpose {
out.push(e);
}
}
Stmt::SheetRangePaste { sheet, .. } => out.push(sheet),
Stmt::SheetProtection { sheet, ui_only, .. } => {
out.push(sheet);
if let Some(e) = ui_only {
out.push(e);
}
}
Stmt::RangeClear { .. } => {}
Stmt::RangeOffsetWrite {
row_off,
col_off,
value,
..
} => {
out.push(row_off);
out.push(col_off);
out.push(value);
}
Stmt::RangeDelete { .. } => {}
Stmt::RangeInsert { .. } => {}
Stmt::RowColDelete { index, .. } => out.push(index),
Stmt::RowColInsert { index, .. } => out.push(index),
Stmt::RangeSort { .. } => {}
Stmt::RangeAutoFilter {
field, criteria1, ..
} => {
if let Some(e) = field {
out.push(e);
}
if let Some(e) = criteria1 {
out.push(e);
}
}
Stmt::RangeName { .. } => {}
Stmt::SheetCellWrite {
sheet,
row,
col,
value,
} => {
out.push(sheet);
out.push(row);
out.push(col);
out.push(value);
}
Stmt::SheetRangeWrite { sheet, value, .. } => {
out.push(sheet);
out.push(value);
}
Stmt::WithSheet { .. } => {}
Stmt::SheetsAdd => {}
Stmt::SheetsDelete { sheet } => out.push(sheet),
Stmt::For { from, to, step, .. } => {
out.push(from);
out.push(to);
if let Some(s) = step {
out.push(s);
}
}
Stmt::ForEach { .. } => {}
Stmt::If { condition, .. } => out.push(condition),
Stmt::DoLoop {
pre_cond,
post_cond,
..
} => {
if let Some((_, e)) = pre_cond {
out.push(e);
}
if let Some((_, e)) = post_cond {
out.push(e);
}
}
Stmt::SelectCase { expr, cases, .. } => {
out.push(expr);
for (matches, _) in cases {
for m in matches {
match m {
CaseMatch::Value(e) => out.push(e),
CaseMatch::Range(a, b) => {
out.push(a);
out.push(b);
}
CaseMatch::IsOp(_, e) => out.push(e),
}
}
}
}
Stmt::ExitFor | Stmt::ExitDo | Stmt::ExitSub | Stmt::ExitFunction => {}
Stmt::OnError { .. } => {}
Stmt::OnErrorGoTo(_) => {}
Stmt::Label(_) => {}
Stmt::GoTo(_) => {}
Stmt::Resume { .. } => {}
Stmt::CallSub { args, .. } => {
for a in args {
out.push(a);
}
}
Stmt::Dim => {}
Stmt::DimBare { .. } => {}
Stmt::DimArray { sizes, .. } => {
for d in sizes {
out.push(&d.upper);
if let Some(lo) = &d.lower {
out.push(lo);
}
}
}
Stmt::ReDim { sizes, .. } => {
for d in sizes {
out.push(&d.upper);
if let Some(lo) = &d.lower {
out.push(lo);
}
}
}
Stmt::Erase { .. } => {}
Stmt::ArrayWrite { indices, value, .. } => {
for i in indices {
out.push(i);
}
out.push(value);
}
Stmt::With { target, .. } => {
if let WithTarget::Cells(row, col) = target {
out.push(row);
out.push(col);
}
}
Stmt::WithDot { member, value } => {
if let WithMember::Cells { row, col, .. } = member {
out.push(row);
out.push(col);
}
out.push(value);
}
Stmt::MsgBox { message } => out.push(message),
Stmt::RecordSet { value, .. } => out.push(value),
Stmt::DimRecord { .. } => {}
Stmt::DimArrayRecord { sizes, .. } => {
for s in sizes {
out.push(s);
}
}
Stmt::DimMulti(decls) => {
for d in decls {
collect_stmt_exprs(d, out);
}
}
Stmt::RecordSetNested { value, .. } => out.push(value),
Stmt::ArrayRecordSet { indices, value, .. } => {
for i in indices {
out.push(i);
}
out.push(value);
}
Stmt::Unsupported { .. } => {}
Stmt::ErrClear => {}
Stmt::ErrRaise {
number,
source,
description,
help_file,
help_context,
} => {
out.push(number);
if let Some(e) = source {
out.push(e);
}
if let Some(e) = description {
out.push(e);
}
if let Some(e) = help_file {
out.push(e);
}
if let Some(e) = help_context {
out.push(e);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn walk_expr(
expr: &Expr,
prog: &Program,
local_names: &HashSet<String>,
other_module_names: &HashSet<String>,
stmt_span: SourceSpan,
source: &str,
file: &str,
diags: &mut Vec<Diagnostic>,
) {
match expr {
Expr::FuncCall { name, args } => {
if !is_resolvable(name, prog, local_names, other_module_names) {
diags.push(Diagnostic {
severity: "error",
code: "E1002",
kind: "undefined_sub_or_function",
message: format!("Unknown VBA function: '{}'", name),
location: Some(locate(source, file, stmt_span)),
});
}
for a in args {
walk_expr(
a,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
}
Expr::BinOp { lhs, rhs, .. } => {
walk_expr(
lhs,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
rhs,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::UnaryMinus(e) | Expr::UnaryNot(e) => walk_expr(
e,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
),
Expr::CellRead { row, col } => {
walk_expr(
row,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
col,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::RangeOffsetRead {
row_off, col_off, ..
} => {
walk_expr(
row_off,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
col_off,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::CellsFind { what, .. } => walk_expr(
what,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
),
Expr::SheetCellRead { sheet, row, col } => {
walk_expr(
sheet,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
row,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
col,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::SheetRangeRead { sheet, .. } => {
walk_expr(
sheet,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::WorkbookQualifiedSheet { workbook, sheet } => {
walk_expr(
workbook,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
sheet,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::CellsEndProp { row, col, .. } => {
walk_expr(
row,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
walk_expr(
col,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
Expr::ArrayRecordGet { indices, .. } => {
for i in indices {
walk_expr(
i,
prog,
local_names,
other_module_names,
stmt_span,
source,
file,
diags,
);
}
}
Expr::Integer(_)
| Expr::Float(_)
| Expr::Str(_)
| Expr::Bool(_)
| Expr::Var(_)
| Expr::RangeRead { .. }
| Expr::RowsCount
| Expr::ColsCount
| Expr::ActiveSheetRef
| Expr::ObjectVarSheet(_)
| Expr::RecordGet { .. }
| Expr::RecordGetNested { .. }
| Expr::IsNothing(_)
| Expr::WithDot(_)
| Expr::ErrNumber
| Expr::ErrDescription
| Expr::ErrSource
| Expr::ErrHelpFile
| Expr::ErrHelpContext => {}
}
}
pub fn all_ok(diags: &[Diagnostic]) -> bool {
!diags.iter().any(|d| d.severity == "error")
}
pub fn diagnostics_to_json(diags: &[Diagnostic]) -> String {
let items: Vec<String> = diags.iter().map(diagnostic_to_json).collect();
format!(
"{{\"schema_version\":1,\"ok\":{},\"diagnostics\":[{}]}}",
all_ok(diags),
items.join(","),
)
}
fn diagnostic_to_json(d: &Diagnostic) -> String {
let location_json = match &d.location {
Some(loc) => format!(
"{{\"file\":{},\"line\":{},\"column\":{}}}",
json_string(&loc.file),
loc.line,
loc.column,
),
None => "null".to_string(),
};
format!(
"{{\"severity\":{},\"code\":{},\"kind\":{},\"message\":{},\"location\":{}}}",
json_string(d.severity),
json_string(d.code),
json_string(d.kind),
json_string(&d.message),
location_json,
)
}
#[cfg(test)]
mod tests {
use super::*;
fn codes(diags: &[Diagnostic]) -> Vec<&str> {
diags.iter().map(|d| d.code).collect()
}
#[test]
fn clean_program_has_no_diagnostics() {
let diags = run_check(
"Sub Main()\n Cells(1, 1).Value = 1\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
assert!(all_ok(&diags));
}
#[test]
fn parse_error_short_circuits_everything_else() {
let diags = run_check("Sub Main(\n x = 1\n", "f.bas", Some("Main"));
assert_eq!(codes(&diags), vec!["E2001"]);
assert_eq!(diags[0].severity, "error");
assert!(diags[0].location.is_some());
assert!(!all_ok(&diags));
}
#[test]
fn missing_entrypoint_is_reported() {
let diags = run_check(
"Sub Main()\n x = 1\nEnd Sub\n",
"f.bas",
Some("DoesNotExist"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
assert_eq!(diags[0].kind, "undefined_sub_or_function");
assert!(diags[0].location.is_none());
}
#[test]
fn entrypoint_check_is_case_insensitive() {
let diags = run_check("Sub Main()\n x = 1\nEnd Sub\n", "f.bas", Some("MAIN"));
assert!(diags.is_empty());
}
#[test]
fn no_macro_name_skips_entrypoint_check() {
let diags = run_check("Sub Main()\n x = 1\nEnd Sub\n", "f.bas", None);
assert!(diags.is_empty());
}
#[test]
fn top_level_msgbox_is_detected() {
let diags = run_check(
"Sub Main()\n MsgBox \"hi\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1001"]);
assert_eq!(diags[0].severity, "info");
assert_eq!(diags[0].location.as_ref().unwrap().line, 2);
assert!(all_ok(&diags)); }
#[test]
fn msgbox_nested_inside_if_and_for_is_detected() {
let diags = run_check(
"Sub Main()\n\
\x20 For i = 1 To 3\n\
\x20 If i = 2 Then\n\
\x20 MsgBox \"two\"\n\
\x20 End If\n\
\x20 Next i\n\
End Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1001"]);
}
#[test]
fn multiple_msgbox_calls_are_all_reported_in_order() {
let diags = run_check(
"Sub Main()\n MsgBox \"one\"\n MsgBox \"two\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1001", "I1001"]);
assert_eq!(diags[0].location.as_ref().unwrap().line, 2);
assert_eq!(diags[1].location.as_ref().unwrap().line, 3);
}
#[test]
fn json_shape_round_trips_severity_and_ok() {
let diags = run_check(
"Sub Main()\n MsgBox \"hi\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
let json = diagnostics_to_json(&diags);
assert!(json.contains("\"ok\":true"));
assert!(json.contains("\"severity\":\"info\""));
assert!(json.contains("\"code\":\"I1001\""));
}
#[test]
fn json_shape_reports_ok_false_when_an_error_is_present() {
let diags = run_check("Sub Main()\n x = 1\nEnd Sub\n", "f.bas", Some("Nope"));
let json = diagnostics_to_json(&diags);
assert!(json.contains("\"ok\":false"));
}
#[test]
fn undefined_callsub_target_is_reported() {
let diags = run_check(
"Sub Main()\n Call Bogus(1)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
assert_eq!(diags[0].message, "Sub/Function 'bogus' not found");
assert_eq!(diags[0].location.as_ref().unwrap().line, 2);
}
#[test]
fn undefined_bare_call_target_is_reported() {
let diags = run_check("Sub Main()\n Bogus()\nEnd Sub\n", "f.bas", Some("Main"));
assert_eq!(codes(&diags), vec!["E1002"]);
}
#[test]
fn undefined_funccall_target_at_top_level_is_reported() {
let diags = run_check(
"Sub Main()\n x = Bogus(1)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
assert_eq!(diags[0].message, "Unknown VBA function: 'bogus'");
}
#[test]
fn undefined_call_nested_inside_an_expression_uses_the_statement_location() {
let diags = run_check(
"Sub Main()\n x = 1 + Bogus(2)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
assert_eq!(diags[0].location.as_ref().unwrap().line, 2);
}
#[test]
fn undefined_call_nested_inside_a_cells_index_is_reported() {
let diags = run_check(
"Sub Main()\n Cells(Bogus(1), 2).Value = 1\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
}
#[test]
fn undefined_call_inside_select_case_condition_is_reported() {
let diags = run_check(
"Sub Main()\n\
\x20 Select Case Bogus(1)\n\
\x20 Case 1\n\
\x20 End Select\n\
End Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
}
#[test]
fn calling_a_real_user_sub_is_not_flagged() {
let diags = run_check(
"Sub Main()\n Call Helper(1)\nEnd Sub\n\
Sub Helper(x)\n y = x\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn calling_a_real_user_function_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = Helper(1)\nEnd Sub\n\
Function Helper(n)\n Helper = n\nEnd Function\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn builtin_vba_function_call_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = Len(\"hi\")\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn worksheet_function_call_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = WorksheetFunction.Sum(Range(\"A1:A2\"))\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn application_worksheet_function_call_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = Application.WorksheetFunction.Sum(Range(\"A1:A2\"))\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn user_function_shadowing_a_builtin_name_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = Len(1)\nEnd Sub\n\
Function Len(n)\n Len = n\nEnd Function\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn indexing_a_split_result_is_not_flagged() {
let diags = run_check(
"Sub Main()\n parts = Split(\"a,b,c\", \",\")\n x = parts(0)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty(), "{:?}", diags[0].message);
}
#[test]
fn indexing_a_dim_array_is_not_flagged() {
let diags = run_check(
"Sub Main()\n Dim arr(10)\n arr(0) = 1\n x = arr(0)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty(), "{:?}", diags[0].message);
}
#[test]
fn indexing_a_function_parameter_array_is_not_flagged() {
let diags = run_check(
"Sub Main()\n x = Helper(1)\nEnd Sub\n\
Function Helper(arr)\n Helper = arr(0)\nEnd Function\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty(), "{:?}", diags[0].message);
}
#[test]
fn a_genuinely_undefined_call_is_still_reported_alongside_a_real_array() {
let diags = run_check(
"Sub Main()\n Dim arr(10)\n arr(0) = 1\n x = Bogus(arr(0))\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
assert_eq!(diags[0].message, "Unknown VBA function: 'bogus'");
}
#[test]
fn debug_print_is_an_unsupported_construct_diagnostic() {
let diags = run_check(
"Sub Main()\n Debug.Print \"hi\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert_eq!(diags[0].severity, "info");
assert_eq!(diags[0].kind, "unsupported_construct");
assert!(
diags[0].message.contains("Debug.Print"),
"{:?}",
diags[0].message
);
assert_eq!(diags[0].location.as_ref().unwrap().line, 2);
}
#[test]
fn unrecognized_range_property_is_an_unsupported_construct_diagnostic() {
let diags = run_check(
"Sub Main()\n Range(\"A1\").NumberFormat = \"0.00\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert!(
diags[0].message.contains("numberformat"),
"{:?}",
diags[0].message
);
}
#[test]
fn unrecognized_sheets_method_is_an_unsupported_construct_diagnostic() {
let diags = run_check(
"Sub Main()\n Sheets.Foo\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert!(
diags[0].message.contains("Sheets.foo"),
"{:?}",
diags[0].message
);
}
#[test]
fn bare_statement_call_is_an_unsupported_construct_diagnostic() {
let diags = run_check("Sub Main()\n Foo\nEnd Sub\n", "f.bas", Some("Main"));
assert_eq!(codes(&diags), vec!["I1002"]);
assert!(diags[0].message.contains("'foo'"), "{:?}", diags[0].message);
}
#[test]
fn unsupported_construct_nested_inside_if_and_for_is_detected() {
let diags = run_check(
"Sub Main()\n\
\x20 For i = 1 To 3\n\
\x20 If i = 2 Then\n\
\x20 Debug.Print \"two\"\n\
\x20 End If\n\
\x20 Next i\n\
End Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
}
#[test]
fn unsupported_construct_alone_is_still_ok() {
let diags = run_check(
"Sub Main()\n Debug.Print \"hi\"\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(all_ok(&diags));
}
#[test]
fn unsupported_construct_coexists_with_a_real_error() {
let diags = run_check(
"Sub Main()\n Debug.Print \"hi\"\n x = Bogus(1)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002", "E1002"]);
assert!(!all_ok(&diags));
}
#[test]
fn module_level_const_with_modifier_is_an_unsupported_construct_diagnostic() {
let diags = run_check(
"Public Const MAX_RETRIES = 5\nSub Main()\n a = 1\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert_eq!(diags[0].severity, "info");
assert!(diags[0].message.contains("Const"), "{:?}", diags[0].message);
assert_eq!(diags[0].location.as_ref().unwrap().line, 1);
assert!(all_ok(&diags));
}
#[test]
fn module_level_unrecognized_line_is_an_unsupported_construct_diagnostic() {
let diags = run_check(
"Declare Function Foo Lib \"x.dll\" ()\nSub Main()\n a = 1\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert!(
diags[0].message.contains("declare"),
"{:?}",
diags[0].message
);
}
#[test]
fn module_level_plain_declaration_is_not_flagged() {
let diags = run_check(
"Public x As Long\nSub Main()\n x = 1\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn unsupported_construct_nested_inside_with_record_is_detected() {
let diags = run_check(
"Sub Main()\n With p\n .Field\n End With\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["I1002"]);
assert!(
diags[0].message.contains(".field"),
"{:?}",
diags[0].message
);
}
#[test]
fn cross_module_call_is_not_flagged_when_other_module_names_are_given() {
let mut others = HashSet::new();
others.insert("helper".to_string());
let diags = run_check_in_project(
"Sub Main()\n Call Helper()\nEnd Sub\n",
"module2.bas",
Some("Main"),
&others,
);
assert!(
diags.is_empty(),
"expected no diagnostics, got {} entries",
diags.len()
);
}
#[test]
fn genuinely_undefined_call_is_still_flagged_in_project_mode() {
let others = HashSet::new();
let diags = run_check_in_project(
"Sub Main()\n Call Bogus()\nEnd Sub\n",
"module2.bas",
Some("Main"),
&others,
);
assert_eq!(codes(&diags), vec!["E1002"]);
}
#[test]
fn run_check_without_project_context_still_flags_a_cross_module_name() {
let diags = run_check(
"Sub Main()\n Call Helper()\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
}
fn parse_ok(src: &str) -> Program {
parser::parse_with_span(src).unwrap_or_else(|e| panic!("should parse: {}", e.message))
}
fn compile_errors(src: &str) -> Option<(String, SourceSpan)> {
compile_check_errors(&parse_ok(src), &HashSet::new())
}
#[test]
fn clean_program_has_no_compile_errors() {
assert!(compile_errors("Sub Main()\n x = 1\nEnd Sub\n").is_none());
}
#[test]
fn undefined_sub_call_is_a_compile_error() {
let (msg, _) = compile_errors("Sub Main()\n Call Helper()\nEnd Sub\n").unwrap();
assert_eq!(msg, "Sub/Function 'helper' not found");
}
#[test]
fn undefined_function_used_in_an_expression_is_a_compile_error() {
let (msg, _) = compile_errors("Sub Main()\n x = Helper(1)\nEnd Sub\n").unwrap();
assert_eq!(msg, "Unknown VBA function: 'helper'");
}
#[test]
fn undefined_call_nested_inside_another_call_is_still_caught() {
let (msg, _) = compile_errors(
"Function Foo(n)\n Foo = n\nEnd Function\nSub Main()\n x = Foo(Bar(1))\nEnd Sub\n",
)
.unwrap();
assert_eq!(msg, "Unknown VBA function: 'bar'");
}
#[test]
fn defined_sub_call_with_the_right_arg_count_is_not_a_compile_error() {
assert!(compile_errors(
"Sub Helper(a, b)\n x = a + b\nEnd Sub\nSub Main()\n Call Helper(1, 2)\nEnd Sub\n"
)
.is_none());
}
#[test]
fn too_few_arguments_to_a_same_module_sub_is_a_compile_error() {
let (msg, _) = compile_errors(
"Sub Helper(a, b)\n x = a + b\nEnd Sub\nSub Main()\n Call Helper(1)\nEnd Sub\n",
)
.unwrap();
assert_eq!(msg, "'helper' expects 2 argument(s), got 1");
}
#[test]
fn too_many_arguments_to_a_same_module_function_is_a_compile_error() {
let (msg, _) = compile_errors(
"Function Helper(a)\n Helper = a\nEnd Function\nSub Main()\n x = Helper(1, 2)\nEnd Sub\n",
)
.unwrap();
assert_eq!(msg, "'helper' expects 1 argument(s), got 2");
}
#[test]
fn array_index_read_is_not_mistaken_for_an_argument_count_mismatch() {
assert!(
compile_errors(
"Sub Main()\n Dim arr(3, 3)\n arr(1, 1) = 5\n x = arr(1, 1)\nEnd Sub\n"
)
.is_none()
);
}
#[test]
fn goto_to_an_undefined_label_is_a_compile_error() {
let (msg, _) = compile_errors("Sub Main()\n GoTo Nowhere\nEnd Sub\n").unwrap();
assert_eq!(msg, "GoTo: label 'nowhere' not found");
}
#[test]
fn goto_to_a_label_declared_later_in_the_same_body_is_not_a_compile_error() {
assert!(
compile_errors("Sub Main()\n GoTo Skip\n x = 1\nSkip:\n y = 2\nEnd Sub\n")
.is_none()
);
}
#[test]
fn goto_to_a_label_nested_inside_an_if_block_is_not_a_compile_error() {
assert!(
compile_errors(concat!(
"Sub Main()\n",
" If True Then\n",
" GoTo Inner\n",
" End If\n",
" If False Then\n",
"Inner:\n",
" y = 2\n",
" End If\n",
"End Sub\n",
))
.is_none()
);
}
#[test]
fn on_error_goto_an_undefined_label_is_a_compile_error() {
let (msg, _) = compile_errors("Sub Main()\n On Error GoTo Nowhere\nEnd Sub\n").unwrap();
assert_eq!(msg, "On Error GoTo: label 'nowhere' not found");
}
#[test]
fn on_error_goto_a_real_label_is_not_a_compile_error() {
assert!(compile_errors(
"Sub Main()\n On Error GoTo Handler\n x = 1\n Exit Sub\nHandler:\n y = 2\nEnd Sub\n"
)
.is_none());
}
#[test]
fn a_deliberately_unimplemented_worksheet_function_is_not_flagged() {
let (msg, _) = compile_errors(
"Sub Main()\n x = WorksheetFunction.TextJoin(\",\", True, \"a\", \"b\")\nEnd Sub\n",
)
.unwrap();
assert_eq!(msg, "WorksheetFunction.textjoin is not implemented");
}
#[test]
fn a_call_to_a_name_in_another_module_is_not_flagged_when_registered() {
let mut others = HashSet::new();
others.insert("helper".to_string());
let prog = parse_ok("Sub Main()\n Call Helper()\nEnd Sub\n");
assert!(compile_check_errors(&prog, &others).is_none());
}
#[test]
fn a_cross_module_call_is_not_arg_count_checked_since_its_arity_is_unknown_here() {
let mut others = HashSet::new();
others.insert("helper".to_string());
let prog = parse_ok("Sub Main()\n Call Helper(1, 2, 3)\nEnd Sub\n");
assert!(compile_check_errors(&prog, &others).is_none());
}
#[test]
fn first_violation_wins_when_a_program_has_several() {
let prog = parse_ok(concat!(
"Sub Main()\n",
" Call FirstUndefined()\n",
"End Sub\n",
"Sub Second()\n",
" Call SecondUndefined()\n",
"End Sub\n",
));
let (msg, _) = compile_check_errors(&prog, &HashSet::new()).unwrap();
assert_eq!(msg, "Sub/Function 'firstundefined' not found");
}
#[test]
fn run_check_reports_an_argument_count_mismatch_as_e1008() {
let diags = run_check(
"Sub Helper(a, b)\n x = a + b\nEnd Sub\nSub Main()\n Call Helper(1)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1008"]);
assert_eq!(diags[0].kind, "argument_count_mismatch");
assert_eq!(diags[0].message, "'helper' expects 2 argument(s), got 1");
assert!(diags[0].location.is_some());
}
#[test]
fn run_check_reports_an_undefined_goto_label_as_e1009() {
let diags = run_check(
"Sub Main()\n GoTo Nowhere\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1009"]);
assert_eq!(diags[0].kind, "undefined_label");
assert_eq!(diags[0].message, "GoTo: label 'nowhere' not found");
}
#[test]
fn run_check_reports_an_undefined_on_error_goto_label_as_e1009() {
let diags = run_check(
"Sub Main()\n On Error GoTo Nowhere\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1009"]);
assert_eq!(diags[0].message, "On Error GoTo: label 'nowhere' not found");
}
#[test]
fn run_check_does_not_double_report_an_undefined_call_as_both_e1002_and_e1008() {
let diags = run_check(
"Sub Main()\n Call DoesNotExist(1, 2, 3)\nEnd Sub\n",
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1002"]);
}
#[test]
fn run_check_finds_every_argument_count_mismatch_not_just_the_first() {
let diags = run_check(
concat!(
"Sub Helper(a, b)\n",
" x = a + b\n",
"End Sub\n",
"Sub Main()\n",
" Call Helper(1)\n",
" Call Helper(1, 2, 3)\n",
"End Sub\n",
),
"f.bas",
Some("Main"),
);
assert_eq!(codes(&diags), vec!["E1008", "E1008"]);
}
#[test]
fn run_check_does_not_flag_a_correct_program_with_the_new_checks() {
let diags = run_check(
concat!(
"Sub Helper(a, b)\n",
" x = a + b\n",
"End Sub\n",
"Sub Main()\n",
" On Error GoTo Handler\n",
" Call Helper(1, 2)\n",
" GoTo Skip\n",
"Handler:\n",
" y = 1\n",
"Skip:\n",
" z = 2\n",
"End Sub\n",
),
"f.bas",
Some("Main"),
);
assert!(diags.is_empty());
}
#[test]
fn run_check_and_compile_check_errors_agree_on_the_exact_same_program() {
let src =
"Sub Helper(a, b)\n x = a + b\nEnd Sub\nSub Main()\n Call Helper(1)\nEnd Sub\n";
let diags = run_check(src, "f.bas", Some("Main"));
let (compile_msg, _) = compile_errors(src).unwrap();
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, compile_msg);
}
}