use crate::errors::ModuleError;
use crate::language::Token;
use crate::parsers::SyntaxKind;
use crate::parsers::cst::Parser;
impl Parser<'_> {
pub(crate) fn parse_call_statement(&mut self) {
self.parsing_header = false;
self.builder.start_node(SyntaxKind::CallStatement.to_raw());
self.consume_whitespace();
self.consume_token();
self.consume_whitespace();
self.parse_call_target();
self.consume_whitespace();
if self.at_token(Token::LeftParenthesis) {
self.parse_parenthesized_arguments();
}
if self.at_token(Token::Newline) {
self.consume_token();
}
self.builder.finish_node(); }
pub(crate) fn parse_procedure_call(&mut self) {
self.parsing_header = false;
self.builder.start_node(SyntaxKind::CallStatement.to_raw());
self.consume_whitespace();
let is_print_like_call = self.parse_call_target();
let has_whitespace_before_paren = self.at_token(Token::Whitespace);
self.consume_whitespace();
if self.at_token(Token::LeftParenthesis) {
if has_whitespace_before_paren {
self.parse_unparenthesized_arguments(is_print_like_call);
} else {
self.parse_parenthesized_arguments();
}
} else if !self.at_token(Token::Newline) && !self.is_at_end() {
self.parse_unparenthesized_arguments(is_print_like_call);
}
if self.at_token(Token::Newline) {
self.consume_token();
}
self.builder.finish_node(); }
fn parse_call_target(&mut self) -> bool {
let mut last_name_is_print = false;
if self.at_token(Token::PeriodOperator) {
self.consume_token();
self.consume_whitespace();
}
if self.is_identifier() || self.at_keyword() {
last_name_is_print = self.current_token_is_print_name();
self.consume_token();
}
loop {
let mut lookahead = 0;
let mut found_whitespace = false;
while let Some((_, token)) = self.tokens.get(self.pos + lookahead) {
if *token == Token::Whitespace {
lookahead += 1;
found_whitespace = true;
} else {
break;
}
}
if let Some((_, token)) = self.tokens.get(self.pos + lookahead) {
if *token == Token::PeriodOperator {
if found_whitespace {
for _ in 0..lookahead {
self.consume_token();
}
}
self.consume_token();
self.consume_whitespace();
if self.is_identifier() || self.at_keyword() {
last_name_is_print = self.current_token_is_print_name();
self.consume_token();
} else {
break;
}
} else {
break;
}
} else {
break;
}
}
last_name_is_print
}
fn current_token_is_print_name(&self) -> bool {
if let Some((text, token)) = self.tokens.get(self.pos) {
*token == Token::PrintKeyword || text.eq_ignore_ascii_case("print")
} else {
false
}
}
fn parse_parenthesized_arguments(&mut self) {
self.consume_token();
self.builder.start_node(SyntaxKind::ArgumentList.to_raw());
self.consume_whitespace();
while !self.is_at_end() && !self.at_token(Token::RightParenthesis) {
self.builder.start_node(SyntaxKind::Argument.to_raw());
if !self.at_token(Token::Comma) && !self.at_token(Token::RightParenthesis) {
if self.at_token(Token::ByValKeyword) || self.at_token(Token::ByRefKeyword) {
self.consume_token();
self.consume_whitespace();
}
let _ = self.try_consume_named_argument_prefix_for_call_argument();
self.parse_expression();
}
self.builder.finish_node();
self.consume_whitespace();
if self.at_token(Token::Comma) {
self.consume_token();
self.consume_whitespace();
} else {
break;
}
}
self.builder.finish_node();
if self.at_token(Token::RightParenthesis) {
self.consume_token();
}
}
fn parse_unparenthesized_arguments(&mut self, allow_semicolon_separator: bool) {
self.builder.start_node(SyntaxKind::ArgumentList.to_raw());
loop {
if self.at_token(Token::Newline) || self.is_at_end() {
break;
}
self.builder.start_node(SyntaxKind::Argument.to_raw());
if !(self.at_token(Token::Comma)
|| self.at_token(Token::Newline)
|| allow_semicolon_separator && self.at_token(Token::Semicolon))
{
if self.at_token(Token::ByValKeyword) || self.at_token(Token::ByRefKeyword) {
self.consume_token();
self.consume_whitespace();
}
let _ = self.try_consume_named_argument_prefix_for_call_argument();
self.parse_expression();
}
self.builder.finish_node();
self.consume_whitespace();
if self.at_token(Token::Comma)
|| (allow_semicolon_separator && self.at_token(Token::Semicolon))
{
self.consume_token();
self.consume_whitespace();
} else {
if !allow_semicolon_separator && self.at_token(Token::Semicolon) {
self.report_error(ModuleError::InvalidSemicolonSeparatorInProcedureCall);
}
break;
}
}
self.builder.finish_node(); }
fn try_consume_named_argument_prefix_for_call_argument(&mut self) -> bool {
let mut idx = self.pos;
let Some((_, first_token)) = self.tokens.get(idx) else {
return false;
};
if !(*first_token == Token::Identifier || first_token.is_keyword()) {
return false;
}
idx += 1;
while let Some((_, Token::Whitespace)) = self.tokens.get(idx) {
idx += 1;
}
if self.tokens.get(idx).map(|(_, token)| *token) != Some(Token::ColonOperator) {
return false;
}
idx += 1;
while let Some((_, Token::Whitespace)) = self.tokens.get(idx) {
idx += 1;
}
if self.tokens.get(idx).map(|(_, token)| *token) != Some(Token::EqualityOperator) {
return false;
}
while self.pos <= idx {
self.consume_token();
}
self.consume_whitespace();
true
}
pub(crate) fn is_at_procedure_call(&self) -> bool {
if self.at_token(Token::Identifier) {
} else if self.at_keyword() {
if let Some(
Token::EndKeyword
| Token::ExitKeyword
| Token::LoopKeyword
| Token::NextKeyword
| Token::WendKeyword
| Token::ElseKeyword
| Token::ElseIfKeyword
| Token::CaseKeyword
| Token::IfKeyword
| Token::ThenKeyword
| Token::SelectKeyword
| Token::DoKeyword
| Token::WhileKeyword
| Token::UntilKeyword
| Token::ForKeyword
| Token::ToKeyword
| Token::StepKeyword
| Token::DeclareKeyword
| Token::SubKeyword
| Token::FunctionKeyword
| Token::PropertyKeyword
| Token::WithKeyword
| Token::ReturnKeyword
| Token::ResumeKeyword,
) = self.current_token()
{
return false;
}
} else {
return false;
}
let mut paren_depth: i32 = 0;
let mut seen_other_operator = false;
let mut seen_top_level_comma = false;
let mut previous_non_whitespace: Option<Token> = None;
for (_text, token) in self.tokens.iter().skip(self.pos) {
match token {
Token::Newline | Token::EndOfLineComment | Token::RemComment => {
return true;
}
Token::ColonOperator if paren_depth == 0 => {
return true;
}
Token::Whitespace => {
continue;
}
Token::LeftParenthesis => {
paren_depth += 1;
}
Token::RightParenthesis => {
paren_depth = paren_depth.saturating_sub(1);
}
Token::EqualityOperator if paren_depth == 0 => {
if previous_non_whitespace == Some(Token::ColonOperator) {
previous_non_whitespace = Some(*token);
continue;
}
if seen_top_level_comma {
return true;
}
return seen_other_operator;
}
Token::Comma if paren_depth == 0 => {
seen_top_level_comma = true;
}
Token::AndKeyword
| Token::OrKeyword
| Token::XorKeyword
| Token::EqvKeyword
| Token::ImpKeyword
| Token::ModKeyword
| Token::NotKeyword
| Token::LessThanOperator
| Token::GreaterThanOperator
| Token::LessThanOrEqualOperator
| Token::GreaterThanOrEqualOperator
| Token::InequalityOperator
| Token::AdditionOperator
| Token::SubtractionOperator
| Token::MultiplicationOperator
| Token::DivisionOperator
| Token::BackwardSlashOperator
| Token::ExponentiationOperator
| Token::Ampersand => {
seen_other_operator = true;
}
_ => {}
}
previous_non_whitespace = Some(*token);
}
true
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn call_statement_simple() {
let source = "Call MySubroutine()\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_with_arguments() {
let source = "Call ProcessData(x, y, z)\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_preserves_whitespace() {
let source = "Call MyFunction ( arg1 , arg2 )\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_in_sub() {
let source = "Sub Main()\nCall DoSomething()\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_no_parentheses() {
let source = "Call MySubroutine\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn multiple_call_statements() {
let source = "Call First()\nCall Second()\nCall Third()\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_with_string_arguments() {
let source = "Call ShowMessage(\"Hello, World!\")\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_with_byval_arguments() {
let source = "RtlMoveMemory vtbl, ByVal pEnumerator, 4\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_with_byval_arguments() {
let source = "Call RtlMoveMemory(vtbl, ByVal pEnumerator, 4)\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn call_statement_with_complex_expressions() {
let source = "Call Calculate(x + y, z * 2, (a - b) / c)\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_no_arguments() {
let source = "InitializeRandomDNA\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_with_parentheses() {
let source = "DoSomething()\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_with_arguments_no_parentheses() {
let source = "MsgBox \"Hello\", vbInformation, \"Title\"\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_with_arguments_with_parentheses() {
let source = "ProcessData(x, y, z)\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn multiple_procedure_calls_in_sub() {
let source = "Sub Test()\nInitializeRandomDNA\nGetInitialSize\nGetInitialSpeed\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_preserves_whitespace() {
let source = "MySub arg1 , arg2\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_vs_assignment() {
let source = "x = 5\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_debug_print_trailing_semicolon() {
let source = "Sub Test()\n Debug.Print Hex(i);\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_debug_print_semicolon_separated_arguments() {
let source = "Sub Test()\n Debug.Print \"A\"; \"B\"\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_output_object_print_trailing_semicolon() {
let source = "Sub Test()\n Printer.Print \"A\";\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_non_print_semicolon_not_separator() {
let source = "Sub Test()\n Foo 1; 2\nEnd Sub\n";
let (cst_opt, failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let failure = &failures[0];
assert!(
matches!(
failure.kind.as_ref(),
crate::errors::ErrorKind::Module(
crate::errors::ModuleError::InvalidSemicolonSeparatorInProcedureCall
)
),
"Expected parser failure for ';' in non-print procedure call"
);
assert_eq!(failure.error_offset, 20);
assert_eq!(failure.line_start, 2);
assert_eq!(failure.line_end, 2);
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_with_named_argument() {
let source = "Sub Test()\n pvRecvBody baBuffer, Flush:=True\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let tree = cst.to_serializable();
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path("../../../../snapshots/syntax/statements/objects/call");
settings.set_prepend_module_to_snapshot(false);
let _guard = settings.bind_to_scope();
insta::assert_yaml_snapshot!(tree);
}
#[test]
fn procedure_call_argument_expression_with_top_level_equals_after_comma() {
let source = "aCopy.Construct ItemLen, CLng(p), , R = 0 And Not mExecBloc\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
assert_eq!(_failures.len(), 0, "Expected no parse failures.");
let cst = cst_opt.expect("CST should be parsed");
let debug = format!("{:#?}", cst.to_serializable());
assert!(
debug.contains("kind: CallStatement"),
"Expected procedure call to parse as CallStatement"
);
assert!(
!debug.contains("kind: Unknown"),
"Did not expect Unknown nodes for call with '=' inside argument expression"
);
}
}