use std::collections::HashMap;
use std::num::NonZeroUsize;
use crate::errors::{ErrorKind, FormError};
use crate::files::common::{
Creatable, Exposed, FileAttributes, FileFormatVersion, NameSpace, ObjectReference,
PreDeclaredID, Properties,
};
use crate::io::{SourceFile, SourceStream};
use crate::language::{
CheckBoxProperties, ComboBoxProperties, CommandButtonProperties, Control, ControlKind,
DataProperties, DirListBoxProperties, DriveListBoxProperties, FileListBoxProperties, Font,
Form, FormProperties, FormRoot, FrameProperties, LabelProperties, ListBoxProperties, MDIForm,
MDIFormProperties, MenuControl, MenuProperties, OptionButtonProperties, PictureBoxProperties,
PropertyGroup, TextBoxProperties, Token,
};
use crate::lexer::{tokenize, TokenStream};
use crate::parsers::SyntaxKind;
use crate::ParseResult;
use either::Either;
use rowan::{GreenNode, GreenNodeBuilder, Language};
use serde::Serialize;
mod assignment;
mod attribute_statements;
mod declarations;
mod deftype_statements;
mod enum_statements;
mod for_statements;
mod function_statements;
mod helpers;
mod if_statements;
mod loop_statements;
mod navigation;
mod option_statements;
mod parameters;
mod properties;
mod property_statements;
mod select_statements;
mod sub_statements;
mod type_statements;
pub use navigation::CstNode;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, Hash)]
pub struct SerializableTree {
pub root: CstNode,
}
pub(crate) fn serialize_cst<S>(cst: &ConcreteSyntaxTree, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
cst.to_serializable().serialize(serializer)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum VB6Language {}
impl Language for VB6Language {
type Kind = SyntaxKind;
fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
SyntaxKind::from_raw(raw)
}
fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
kind.to_raw()
}
}
fn extract_property_groups(groups: &[PropertyGroup]) -> ExtractedGroups {
let mut font = None;
for group in groups {
if group.name.eq_ignore_ascii_case("Font") {
if let Ok(f) = Font::try_from(group) {
font = Some(f);
}
}
}
ExtractedGroups { font }
}
struct ExtractedGroups {
font: Option<Font>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ConcreteSyntaxTree {
root: GreenNode,
}
impl ConcreteSyntaxTree {
fn new(root: GreenNode) -> Self {
Self { root }
}
#[must_use]
pub fn from_source(source_file: &SourceFile) -> ParseResult<'_, Self> {
Self::from_text(
source_file.file_name().to_string(),
source_file.source_stream().contents,
)
}
pub fn from_text<S>(file_name: S, contents: &str) -> ParseResult<'_, Self>
where
S: Into<String>,
{
let mut source_stream = SourceStream::new(file_name.into(), contents);
let token_stream_result = tokenize(&mut source_stream);
let (token_stream_opt, failures) = token_stream_result.unpack();
let Some(token_stream) = token_stream_opt else {
return ParseResult::new(None, failures);
};
let cst = parse(token_stream);
ParseResult::new(Some(cst), failures)
}
#[must_use]
pub fn root_kind(&self) -> SyntaxKind {
SyntaxKind::from_raw(self.root.kind())
}
#[must_use]
pub fn to_serializable(&self) -> SerializableTree {
SerializableTree {
root: self.to_root_node(),
}
}
#[must_use]
pub fn to_root_node(&self) -> CstNode {
CstNode::new(SyntaxKind::Root, self.text(), false, self.children())
}
#[must_use]
pub fn without_kinds(&self, kinds_to_remove: &[SyntaxKind]) -> Self {
let syntax_node = rowan::SyntaxNode::<VB6Language>::new_root(self.root.clone());
let mut builder = GreenNodeBuilder::new();
builder.start_node(SyntaxKind::Root.to_raw());
for child in syntax_node.children_with_tokens() {
let child_kind = match &child {
rowan::NodeOrToken::Node(node) => node.kind(),
rowan::NodeOrToken::Token(token) => token.kind(),
};
if kinds_to_remove.contains(&child_kind) {
continue;
}
Self::clone_node_or_token(&mut builder, child);
}
builder.finish_node();
let new_root = builder.finish();
Self::new(new_root)
}
fn clone_node_or_token(
builder: &mut GreenNodeBuilder<'static>,
node_or_token: rowan::NodeOrToken<
rowan::SyntaxNode<VB6Language>,
rowan::SyntaxToken<VB6Language>,
>,
) {
match node_or_token {
rowan::NodeOrToken::Node(node) => {
builder.start_node(node.kind().to_raw());
for child in node.children_with_tokens() {
Self::clone_node_or_token(builder, child);
}
builder.finish_node();
}
rowan::NodeOrToken::Token(token) => {
builder.token(token.kind().to_raw(), token.text());
}
}
}
}
#[must_use]
pub fn parse(tokens: TokenStream) -> ConcreteSyntaxTree {
let parser = Parser::new(tokens);
parser.parse_root()
}
pub(crate) struct Parser<'a> {
pub(crate) tokens: Vec<(&'a str, Token)>,
pub(crate) pos: usize,
pub(crate) builder: GreenNodeBuilder<'static>,
pub(crate) parsing_header: bool,
}
impl<'a> Parser<'a> {
fn new(token_stream: TokenStream<'a>) -> Self {
Parser {
tokens: token_stream.into_tokens(),
pos: 0,
builder: GreenNodeBuilder::new(),
parsing_header: true,
}
}
pub(crate) fn new_direct_extraction(tokens: Vec<(&'a str, Token)>, pos: usize) -> Self {
Parser {
tokens,
pos,
builder: GreenNodeBuilder::new(),
parsing_header: true,
}
}
pub(crate) fn into_tokens(self) -> Vec<(&'a str, Token)> {
self.tokens[self.pos..].to_vec()
}
pub(crate) fn skip_whitespace(&mut self) {
while self.at_token(Token::Whitespace) {
self.pos += 1;
}
}
pub(crate) fn skip_whitespace_and_newlines(&mut self) {
while self.at_token(Token::Whitespace) || self.at_token(Token::Newline) {
self.pos += 1;
}
}
pub(crate) fn consume_advance(&mut self) -> Option<(&'a str, Token)> {
if self.pos < self.tokens.len() {
let token = self.tokens[self.pos];
self.pos += 1;
Some(token)
} else {
None
}
}
pub(crate) fn parse_version_direct(&mut self) -> ParseResult<'a, FileFormatVersion> {
self.skip_whitespace();
if !self.at_token(Token::VersionKeyword) {
return ParseResult::new(None, Vec::new());
}
self.consume_advance(); self.skip_whitespace();
let version_result = if let Some((text, token)) = self.tokens.get(self.pos) {
match token {
Token::SingleLiteral | Token::DoubleLiteral | Token::IntegerLiteral => {
let version_str = text.trim();
self.consume_advance();
let parts: Vec<&str> = version_str.split('.').collect();
if parts.len() == 2 {
if let (Ok(major), Ok(minor)) =
(parts[0].parse::<u8>(), parts[1].parse::<u8>())
{
Some(FileFormatVersion { major, minor })
} else {
None
}
} else {
None
}
}
_ => None,
}
} else {
None
};
self.skip_whitespace();
if self.at_token(Token::ClassKeyword) {
self.consume_advance();
}
self.skip_whitespace_and_newlines();
ParseResult::new(version_result, Vec::new())
}
fn is_begin_property(&self) -> bool {
if let Some((text, token)) = self.tokens.get(self.pos) {
*token == Token::Identifier && text.eq_ignore_ascii_case("BeginProperty")
} else {
false
}
}
fn is_identifier_text(&self, target: &str) -> bool {
if let Some((text, token)) = self.tokens.get(self.pos) {
*token == Token::Identifier && text.eq_ignore_ascii_case(target)
} else {
false
}
}
fn control_to_menu(control: Control) -> MenuControl {
let (name, tag, index, kind) = control.into_parts();
if let ControlKind::Menu {
properties,
sub_menus,
} = kind
{
MenuControl::new(name, tag, index, properties, sub_menus)
} else {
MenuControl::new(name, tag, index, MenuProperties::default(), Vec::new())
}
}
fn parse_control_type_direct(&mut self) -> String {
let mut parts = Vec::new();
if self.is_identifier() || self.at_keyword() {
if let Some((text, _)) = self.tokens.get(self.pos) {
parts.push(text.to_string());
self.consume_advance();
}
}
while self.at_token(Token::PeriodOperator) {
self.consume_advance(); if self.is_identifier() || self.at_keyword() {
if let Some((text, _)) = self.tokens.get(self.pos) {
parts.push(".".to_string());
parts.push(text.to_string());
self.consume_advance();
}
}
}
parts.join("")
}
fn parse_control_name_direct(&mut self) -> String {
if self.is_identifier() || self.at_keyword() {
if let Some((text, _)) = self.tokens.get(self.pos) {
let name = text.to_string();
self.consume_advance();
return name;
}
}
String::new()
}
fn parse_property_direct(&mut self) -> Option<(String, String)> {
let key = if self.is_identifier() || self.at_keyword() {
if let Some((text, _)) = self.tokens.get(self.pos) {
let k = text.to_string();
self.consume_advance();
k
} else {
return None;
}
} else {
return None;
};
self.skip_whitespace();
if !self.at_token(Token::EqualityOperator) {
return None;
}
self.consume_advance();
self.skip_whitespace();
let mut value_parts = Vec::new();
let mut in_resource_reference = false;
while !self.is_at_end() && !self.at_token(Token::Newline) {
if let Some((text, token)) = self.tokens.get(self.pos) {
let text_copy = *text;
let token_copy = *token;
if token_copy == Token::DollarSign {
value_parts.push(text_copy);
self.consume_advance();
}
else if token_copy == Token::StringLiteral {
value_parts.push(text_copy);
self.consume_advance();
if let Some((_, next_token)) = self.tokens.get(self.pos) {
if *next_token == Token::ColonOperator {
in_resource_reference = true;
}
}
}
else if in_resource_reference && token_copy == Token::ColonOperator {
value_parts.push(text_copy);
self.consume_advance();
}
else if in_resource_reference
&& (token_copy == Token::IntegerLiteral || token_copy == Token::LongLiteral)
{
value_parts.push(text_copy);
self.consume_advance();
break; }
else if token_copy == Token::ColonOperator {
break;
}
else {
value_parts.push(text_copy);
self.consume_advance();
}
} else {
break;
}
}
self.skip_whitespace_and_newlines();
let value = value_parts.concat().trim().to_string();
Some((key, value))
}
fn parse_property_group_direct(&mut self) -> Option<PropertyGroup> {
if !self.is_identifier_text("BeginProperty") {
return None;
}
self.consume_advance(); self.skip_whitespace();
let (name, guid) = self.parse_property_group_name_direct();
self.skip_whitespace_and_newlines();
let mut properties = HashMap::new();
while !self.is_at_end() && !self.is_identifier_text("EndProperty") {
self.skip_whitespace();
if self.is_identifier_text("EndProperty") {
break;
}
if self.is_identifier_text("BeginProperty") {
if let Some(nested_group) = self.parse_property_group_direct() {
properties.insert(nested_group.name.clone(), Either::Right(nested_group));
}
} else if self.is_identifier() || self.at_keyword() {
if let Some((key, value)) = self.parse_property_direct() {
properties.insert(key, Either::Left(value));
}
} else {
self.consume_advance();
}
}
if self.is_identifier_text("EndProperty") {
self.consume_advance();
self.skip_whitespace_and_newlines();
}
Some(PropertyGroup {
name,
guid,
properties,
})
}
fn parse_property_group_name_direct(&mut self) -> (String, Option<uuid::Uuid>) {
let mut name_parts: Vec<&str> = Vec::new();
let mut guid_parts: Vec<&str> = Vec::new();
let mut in_guid = false;
while !self.is_at_end() && !self.at_token(Token::Newline) {
if let Some((text, token)) = self.tokens.get(self.pos) {
if *token == Token::LeftCurlyBrace {
in_guid = true;
} else if *token == Token::RightCurlyBrace {
in_guid = false;
} else if *token != Token::Whitespace && *token != Token::EndOfLineComment {
if in_guid {
guid_parts.push(*text);
} else {
name_parts.push(*text);
}
}
}
self.consume_advance();
}
let name = name_parts.concat();
let guid = if guid_parts.is_empty() {
None
} else {
let guid_str = guid_parts.concat();
uuid::Uuid::parse_str(&guid_str).ok()
};
(name, guid)
}
pub(crate) fn parse_objects_direct(&mut self) -> Vec<ObjectReference> {
let mut objects = Vec::new();
self.skip_whitespace_and_newlines();
while self.at_token(Token::ObjectKeyword) {
if let Some(obj_ref) = self.parse_single_object_direct() {
objects.push(obj_ref);
}
self.skip_whitespace_and_newlines();
}
objects
}
fn parse_single_object_direct(&mut self) -> Option<ObjectReference> {
if !self.at_token(Token::ObjectKeyword) {
return None;
}
self.consume_advance(); self.skip_whitespace();
if !self.at_token(Token::EqualityOperator) {
return None;
}
self.consume_advance(); self.skip_whitespace();
let mut _is_embedded = false;
if self.at_token(Token::MultiplicationOperator) {
self.consume_advance(); if let Some((_text, token)) = self.tokens.get(self.pos) {
if *token == Token::BackwardSlashOperator {
self.consume_advance(); if let Some((text2, token2)) = self.tokens.get(self.pos) {
if *token2 == Token::Identifier && text2.eq_ignore_ascii_case("G") {
self.consume_advance(); _is_embedded = true;
}
}
}
}
}
self.skip_whitespace();
let uuid_part = if let Some((text, token)) = self.tokens.get(self.pos) {
if *token == Token::StringLiteral {
let s = text.trim_matches('"').to_string();
self.consume_advance();
s
} else if *token == Token::LeftCurlyBrace {
let mut parts: Vec<&str> = Vec::new();
while !self.is_at_end() && !self.at_token(Token::Semicolon) {
if let Some((text, token)) = self.tokens.get(self.pos) {
if *token != Token::Whitespace {
parts.push(text);
}
self.consume_advance();
} else {
break;
}
}
parts.concat()
} else {
return None;
}
} else {
return None;
};
self.skip_whitespace();
if !self.at_token(Token::Semicolon) {
return None;
}
self.consume_advance(); self.skip_whitespace();
let file_name = if let Some((text, token)) = self.tokens.get(self.pos) {
if *token == Token::StringLiteral {
let s = text.trim_matches('"').to_string();
self.consume_advance();
s
} else {
return None;
}
} else {
return None;
};
let parts: Vec<&str> = uuid_part.split('#').collect();
if parts.len() >= 3 {
let uuid_str = parts[0].trim_matches(|c| c == '{' || c == '}');
if let Ok(uuid) = uuid::Uuid::parse_str(uuid_str) {
let version = parts[1].to_string();
let unknown1 = parts[2].to_string();
return Some(ObjectReference::Compiled {
uuid,
version,
unknown1,
file_name,
});
}
}
None
}
pub(crate) fn parse_attributes_direct(&mut self) -> FileAttributes {
let mut name = String::new();
let mut global_name_space = NameSpace::Local;
let mut creatable = Creatable::True;
let mut predeclared_id = PreDeclaredID::False;
let mut exposed = Exposed::False;
let mut description: Option<String> = None;
let mut ext_key: HashMap<String, String> = HashMap::new();
self.skip_whitespace_and_newlines();
while self.at_token(Token::AttributeKeyword) {
if let Some((key, value)) = self.parse_single_attribute_direct() {
match key.as_str() {
"VB_Name" => {
name = value;
}
"VB_GlobalNameSpace" => {
global_name_space = if value == "True" || value == "-1" {
NameSpace::Global
} else {
NameSpace::Local
};
}
"VB_Creatable" => {
creatable = if value == "True" || value == "-1" {
Creatable::True
} else {
Creatable::False
};
}
"VB_PredeclaredId" => {
predeclared_id = if value == "True" || value == "-1" {
PreDeclaredID::True
} else {
PreDeclaredID::False
};
}
"VB_Exposed" => {
exposed = if value == "True" || value == "-1" {
Exposed::True
} else {
Exposed::False
};
}
"VB_Description" => {
description = Some(value);
}
_ => {
ext_key.insert(key, value);
}
}
}
self.skip_whitespace_and_newlines();
}
FileAttributes {
name,
global_name_space,
creatable,
predeclared_id,
exposed,
description,
ext_key,
}
}
fn parse_single_attribute_direct(&mut self) -> Option<(String, String)> {
if !self.at_token(Token::AttributeKeyword) {
return None;
}
self.consume_advance(); self.skip_whitespace();
let key = if let Some((text, token)) = self.tokens.get(self.pos) {
if *token == Token::Identifier {
let k = text.to_string();
self.consume_advance();
k
} else {
return None;
}
} else {
return None;
};
self.skip_whitespace();
if !self.at_token(Token::EqualityOperator) {
return None;
}
self.consume_advance(); self.skip_whitespace();
let mut value = String::new();
let mut found_value = false;
if self.at_token(Token::SubtractionOperator) {
value.push('-');
self.consume_advance();
self.skip_whitespace();
}
if let Some((text, token)) = self.tokens.get(self.pos) {
match token {
Token::StringLiteral => {
value.push_str(text.trim().trim_matches('"'));
self.consume_advance();
found_value = true;
}
Token::TrueKeyword => {
value.push_str("True");
self.consume_advance();
found_value = true;
}
Token::FalseKeyword => {
value.push_str("False");
self.consume_advance();
found_value = true;
}
Token::IntegerLiteral | Token::LongLiteral => {
value.push_str(text.trim());
self.consume_advance();
found_value = true;
}
_ => {}
}
}
while self.pos < self.tokens.len() {
if let Some((_, token)) = self.tokens.get(self.pos) {
if *token == Token::Newline {
break;
}
self.consume_advance();
} else {
break;
}
}
if found_value {
Some((key, value))
} else {
None
}
}
fn build_form_root(
control_type: &str,
control_name: String,
tag: String,
index: i32,
properties: Properties,
groups: &[PropertyGroup],
child_controls: Vec<Control>,
menus: Vec<MenuControl>,
) -> Result<FormRoot, ErrorKind> {
match control_type {
"VB.Form" => {
let mut form_properties: FormProperties = properties.into();
let extracted_groups = extract_property_groups(groups);
if let Some(font) = extracted_groups.font {
form_properties.font = Some(font);
}
Ok(FormRoot::Form(Form {
name: control_name,
tag,
index,
properties: form_properties,
controls: child_controls,
menus,
}))
}
"VB.MDIForm" => {
let mut mdi_form_properties: MDIFormProperties = properties.into();
let extracted_groups = extract_property_groups(groups);
if let Some(font) = extracted_groups.font {
mdi_form_properties.font = Some(font);
}
Ok(FormRoot::MDIForm(MDIForm {
name: control_name,
tag,
index,
properties: mdi_form_properties,
controls: child_controls,
menus,
}))
}
_ => Err(ErrorKind::Form(FormError::InvalidTopLevelControl {
control_type: control_type.to_string(),
})),
}
}
fn build_control_kind(
control_type: &str,
properties: Properties,
child_controls: Vec<Control>,
menus: Vec<MenuControl>,
property_groups: Vec<PropertyGroup>,
) -> ControlKind {
use ControlKind;
let groups = extract_property_groups(&property_groups);
match control_type {
"VB.CommandButton" => {
let mut props: CommandButtonProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::CommandButton { properties: props }
}
"VB.Data" => {
let mut props: DataProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::Data { properties: props }
}
"VB.TextBox" => {
let mut props: TextBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::TextBox { properties: props }
}
"VB.Label" => {
let mut props: LabelProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::Label { properties: props }
}
"VB.CheckBox" => {
let mut props: CheckBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::CheckBox { properties: props }
}
"VB.Line" => ControlKind::Line {
properties: properties.into(),
},
"VB.Shape" => ControlKind::Shape {
properties: properties.into(),
},
"VB.ListBox" => {
let mut props: ListBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::ListBox { properties: props }
}
"VB.ComboBox" => {
let mut props: ComboBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::ComboBox { properties: props }
}
"VB.Timer" => ControlKind::Timer {
properties: properties.into(),
},
"VB.HScrollBar" => ControlKind::HScrollBar {
properties: properties.into(),
},
"VB.VScrollBar" => ControlKind::VScrollBar {
properties: properties.into(),
},
"VB.Frame" => {
let mut props: FrameProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::Frame {
properties: props,
controls: child_controls,
}
}
"VB.PictureBox" => {
let mut props: PictureBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::PictureBox {
properties: props,
controls: child_controls,
}
}
"VB.FileListBox" => {
let mut props: FileListBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::FileListBox { properties: props }
}
"VB.DirListBox" => {
let mut props: DirListBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::DirListBox { properties: props }
}
"VB.DriveListBox" => {
let mut props: DriveListBoxProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::DriveListBox { properties: props }
}
"VB.Image" => ControlKind::Image {
properties: properties.into(),
},
"VB.OptionButton" => {
let mut props: OptionButtonProperties = properties.into();
if let Some(font) = groups.font {
props.font = Some(font);
}
ControlKind::OptionButton { properties: props }
}
"VB.OLE" => ControlKind::Ole {
properties: properties.into(),
},
"VB.Menu" => ControlKind::Menu {
properties: properties.into(),
sub_menus: menus,
},
_ => ControlKind::Custom {
properties: properties.into(),
property_groups,
},
}
}
pub(crate) fn parse_properties_block_to_control(&mut self) -> ParseResult<'a, Control> {
self.skip_whitespace();
if !self.at_token(Token::BeginKeyword) {
return ParseResult::new(None, Vec::new());
}
self.consume_advance(); self.skip_whitespace();
let control_type = self.parse_control_type_direct();
self.skip_whitespace();
let control_name = self.parse_control_name_direct();
self.skip_whitespace_and_newlines();
let mut properties = Properties::new();
let mut child_controls = Vec::new();
let mut menus = Vec::new();
let mut property_groups = Vec::new();
let mut failures = Vec::new();
while !self.is_at_end() && !self.at_token(Token::EndKeyword) {
self.skip_whitespace();
if self.at_token(Token::EndKeyword) {
break;
}
if self.at_token(Token::BeginKeyword) {
let child_result = self.parse_properties_block_to_control();
let (child_opt, child_failures) = child_result.unpack();
failures.extend(child_failures);
if let Some(child) = child_opt {
if matches!(child.kind(), ControlKind::Menu { .. }) {
menus.push(Self::control_to_menu(child));
} else {
child_controls.push(child);
}
}
} else if self.is_begin_property() {
if let Some(group) = self.parse_property_group_direct() {
property_groups.push(group);
}
} else if self.is_identifier() || self.at_keyword() {
if let Some((key, value)) = self.parse_property_direct() {
let is_resource_reference = value.contains(':')
&& value
.split(':')
.next_back()
.is_some_and(|part| part.chars().all(|c| c.is_ascii_digit()));
let cleaned_value = if !is_resource_reference
&& value.starts_with('"')
&& value.ends_with('"')
&& value.len() >= 2
{
&value[1..value.len() - 1]
} else {
&value
};
properties.insert(&key, cleaned_value);
}
} else {
self.consume_advance();
}
}
if self.at_token(Token::EndKeyword) {
self.consume_advance();
self.skip_whitespace_and_newlines();
}
let tag = properties.get("Tag").cloned().unwrap_or_default();
let index = properties
.get("Index")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let kind = Self::build_control_kind(
&control_type,
properties,
child_controls,
menus,
property_groups,
);
let control = Control::new(control_name, tag, index, kind);
ParseResult::new(Some(control), failures)
}
pub(crate) fn parse_properties_block_to_form_root(&mut self) -> ParseResult<'a, FormRoot> {
use Properties;
let mut groups = Vec::new();
self.skip_whitespace();
if !self.at_token(Token::BeginKeyword) {
return ParseResult::new(None, Vec::new());
}
self.consume_advance(); self.skip_whitespace();
let control_type = self.parse_control_type_direct();
self.skip_whitespace();
let control_name = self.parse_control_name_direct();
self.skip_whitespace_and_newlines();
let mut properties = Properties::new();
let mut child_controls = Vec::new();
let mut menus = Vec::new();
let mut failures = Vec::new();
while !self.is_at_end() && !self.at_token(Token::EndKeyword) {
self.skip_whitespace();
if self.at_token(Token::EndKeyword) {
break;
}
if self.at_token(Token::BeginKeyword) {
let child_result = self.parse_properties_block_to_control();
let (child_opt, child_failures) = child_result.unpack();
failures.extend(child_failures);
if let Some(child) = child_opt {
if matches!(child.kind(), ControlKind::Menu { .. }) {
menus.push(Self::control_to_menu(child));
} else {
child_controls.push(child);
}
}
} else if self.is_begin_property() {
if let Some(group) = self.parse_property_group_direct() {
groups.push(group);
}
} else if self.is_identifier() || self.at_keyword() {
if let Some((key, value)) = self.parse_property_direct() {
let is_resource_reference = value.contains(':')
&& value
.split(':')
.next_back()
.is_some_and(|part| part.chars().all(|c| c.is_ascii_digit()));
let cleaned_value = if !is_resource_reference
&& value.starts_with('"')
&& value.ends_with('"')
&& value.len() >= 2
{
&value[1..value.len() - 1]
} else {
&value
};
properties.insert(&key, cleaned_value);
}
} else {
self.consume_advance();
}
}
if self.at_token(Token::EndKeyword) {
self.consume_advance();
self.skip_whitespace_and_newlines();
}
let tag = properties.get("Tag").cloned().unwrap_or_default();
let index = properties
.get("Index")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if let Ok(form_root) = Self::build_form_root(
&control_type,
control_name,
tag,
index,
properties,
&groups,
child_controls,
menus,
) {
ParseResult::new(Some(form_root), failures)
} else {
let default_form = FormRoot::Form(Form {
name: String::new(),
tag: String::new(),
index: 0,
properties: FormProperties::default(),
controls: Vec::new(),
menus: Vec::new(),
});
ParseResult::new(Some(default_form), failures)
}
}
fn parse_root(mut self) -> ConcreteSyntaxTree {
self.builder.start_node(SyntaxKind::Root.to_raw());
if self.at_token(Token::VersionKeyword) {
self.parse_version_statement();
}
if self.at_token(Token::BeginKeyword) {
self.parse_properties_block();
}
while self.at_token(Token::AttributeKeyword) {
self.parse_attribute_statement();
}
self.parse_module_body();
self.builder.finish_node();
let root = self.builder.finish();
ConcreteSyntaxTree::new(root)
}
fn parse_module_body(&mut self) {
while !self.is_at_end() {
match self.current_token() {
Some(Token::BeginKeyword) => {
self.parse_properties_block();
}
Some(Token::ObjectKeyword) if self.is_object_statement() => {
self.parse_object_statement();
}
Some(Token::AttributeKeyword) => {
self.parse_attribute_statement();
}
Some(Token::OptionKeyword) => {
if let Some(Token::BaseKeyword) = self.peek_next_keyword() {
self.parse_option_base_statement();
} else if let Some(Token::CompareKeyword) = self.peek_next_keyword() {
self.parse_option_compare_statement();
} else if let Some(Token::PrivateKeyword) = self.peek_next_keyword() {
self.parse_option_private_statement();
} else {
self.parse_option_statement();
}
}
Some(
Token::DefBoolKeyword
| Token::DefByteKeyword
| Token::DefIntKeyword
| Token::DefLngKeyword
| Token::DefCurKeyword
| Token::DefSngKeyword
| Token::DefDblKeyword
| Token::DefDecKeyword
| Token::DefDateKeyword
| Token::DefStrKeyword
| Token::DefObjKeyword
| Token::DefVarKeyword,
) => {
self.parse_deftype_statement();
}
Some(Token::DeclareKeyword) => {
self.parse_declare_statement();
}
Some(Token::EventKeyword) => {
self.parse_event_statement();
}
Some(Token::ImplementsKeyword) => {
self.parse_implements_statement();
}
Some(Token::EnumKeyword) => {
self.parse_enum_statement();
}
Some(Token::TypeKeyword) => {
self.parse_type_statement();
}
Some(Token::SubKeyword) => {
self.parse_sub_statement();
}
Some(Token::FunctionKeyword) => {
self.parse_function_statement();
}
Some(Token::PropertyKeyword) => {
self.parse_property_statement();
}
Some(Token::DimKeyword | Token::ConstKeyword) => {
self.parse_dim();
}
Some(
Token::PrivateKeyword
| Token::PublicKeyword
| Token::FriendKeyword
| Token::StaticKeyword,
) => {
let next_keywords: Vec<_> = self
.peek_next_count_keywords(NonZeroUsize::new(2).unwrap())
.collect();
match next_keywords.as_slice() {
[Token::FunctionKeyword, ..] => self.parse_function_statement(), [Token::SubKeyword, ..] => self.parse_sub_statement(), [Token::PropertyKeyword, ..] => self.parse_property_statement(), [Token::DeclareKeyword, ..] => self.parse_declare_statement(), [Token::EnumKeyword, ..] => self.parse_enum_statement(), [Token::TypeKeyword, ..] => self.parse_type_statement(), [Token::EventKeyword, ..] => self.parse_event_statement(), [Token::ImplementsKeyword, ..] => self.parse_implements_statement(), [Token::StaticKeyword, Token::FunctionKeyword] => {
self.parse_function_statement();
}
[Token::StaticKeyword, Token::SubKeyword] => {
self.parse_sub_statement();
}
[Token::StaticKeyword, Token::PropertyKeyword] => {
self.parse_property_statement();
}
_ => self.parse_dim(),
}
}
_ => {
if matches!(
self.current_token(),
Some(
Token::Whitespace
| Token::Newline
| Token::EndOfLineComment
| Token::RemComment
)
) {
self.consume_token();
} else if self.is_control_flow_keyword() {
self.parse_control_flow_statement();
} else if self.is_library_statement_keyword() {
self.parse_library_statement();
} else if self.is_variable_declaration_keyword() {
self.parse_array_statement();
} else if self.is_statement_keyword() {
self.parse_statement();
} else if self.is_at_label() {
self.parse_label_statement();
} else if self.at_token(Token::LetKeyword) {
self.parse_let_statement();
} else if self.is_at_assignment() {
self.parse_assignment_statement();
} else if self.is_at_procedure_call() {
self.parse_procedure_call();
} else if self.is_identifier() || self.at_keyword() {
self.consume_token();
} else {
self.consume_token_as_unknown();
}
}
}
}
}
fn is_control_flow_keyword(&self) -> bool {
let token = if self.at_token(Token::Whitespace) {
self.peek_next_keyword()
} else {
self.current_token().copied()
};
matches!(
token,
Some(
Token::IfKeyword
| Token::SelectKeyword
| Token::ForKeyword
| Token::DoKeyword
| Token::WhileKeyword
| Token::GotoKeyword
| Token::GoSubKeyword
| Token::ReturnKeyword
| Token::ResumeKeyword
| Token::ExitKeyword
| Token::OnKeyword
)
)
}
fn parse_control_flow_statement(&mut self) {
let token = if self.at_token(Token::Whitespace) {
self.peek_next_keyword()
} else {
self.current_token().copied()
};
match token {
Some(Token::IfKeyword) => {
self.parse_if_statement();
}
Some(Token::SelectKeyword) => {
self.parse_select_case_statement();
}
Some(Token::ForKeyword) => {
let next_kw = if self.at_token(Token::Whitespace) {
self.peek_next_count_keywords(NonZeroUsize::new(2).unwrap())
.nth(1)
} else {
self.peek_next_keyword()
};
if let Some(Token::EachKeyword) = next_kw {
self.parse_for_each_statement();
} else {
self.parse_for_statement();
}
}
Some(Token::DoKeyword) => {
self.parse_do_statement();
}
Some(Token::WhileKeyword) => {
self.parse_while_statement();
}
Some(Token::GotoKeyword) => {
self.parse_goto_statement();
}
Some(Token::GoSubKeyword) => {
self.parse_gosub_statement();
}
Some(Token::ReturnKeyword) => {
self.parse_return_statement();
}
Some(Token::ResumeKeyword) => {
self.parse_resume_statement();
}
Some(Token::ExitKeyword) => {
self.parse_exit_statement();
}
Some(Token::OnKeyword) => {
let next_kw = if self.at_token(Token::Whitespace) {
self.peek_next_count_keywords(NonZeroUsize::new(2).unwrap())
.nth(1)
} else {
self.peek_next_keyword()
};
if let Some(Token::ErrorKeyword) = next_kw {
self.parse_on_error_statement();
} else {
let peek_start = if self.at_token(Token::Whitespace) {
2
} else {
1
};
let keywords: Vec<Token> = self
.peek_next_count_keywords(NonZeroUsize::new(20).unwrap())
.skip(peek_start)
.collect();
let has_goto = keywords.contains(&Token::GotoKeyword);
let has_gosub = keywords.contains(&Token::GoSubKeyword);
if has_goto {
self.parse_on_goto_statement();
} else if has_gosub {
self.parse_on_gosub_statement();
} else {
self.parse_on_error_statement();
}
}
}
_ => {}
}
}
fn is_variable_declaration_keyword(&self) -> bool {
let token = if self.at_token(Token::Whitespace) {
self.peek_next_keyword()
} else {
self.current_token().copied()
};
matches!(token, Some(Token::ReDimKeyword | Token::EraseKeyword))
}
#[allow(clippy::needless_continue)] fn is_object_statement(&self) -> bool {
if !self.at_token(Token::ObjectKeyword) {
return false;
}
let mut found_equals = false;
for (_text, token) in self.tokens.iter().skip(self.pos + 1) {
match token {
Token::Whitespace => continue,
Token::EqualityOperator if !found_equals => {
found_equals = true;
}
Token::StringLiteral | Token::MultiplicationOperator if found_equals => {
return true;
}
_ if found_equals => return false,
Token::Newline | Token::EndOfLineComment | Token::RemComment => {
return false;
}
_ => return false,
}
}
false
}
fn parse_array_statement(&mut self) {
let token = if self.at_token(Token::Whitespace) {
self.peek_next_keyword()
} else {
self.current_token().copied()
};
match token {
Some(Token::ReDimKeyword) => {
self.parse_redim_statement();
}
Some(Token::EraseKeyword) => {
self.parse_erase_statement();
}
_ => {}
}
}
pub(crate) fn parse_statement_list<F>(&mut self, stop_conditions: F)
where
F: Fn(&Parser) -> bool,
{
self.builder.start_node(SyntaxKind::StatementList.to_raw());
while !self.is_at_end() {
if stop_conditions(self) {
break;
}
if self.is_control_flow_keyword() {
self.parse_control_flow_statement();
continue;
}
if self.is_library_statement_keyword() {
self.parse_library_statement();
continue;
}
if self.is_variable_declaration_keyword() {
self.parse_array_statement();
continue;
}
if self.is_statement_keyword() {
self.parse_statement();
continue;
}
match self.current_token() {
Some(
Token::Whitespace
| Token::Newline
| Token::EndOfLineComment
| Token::RemComment,
) => {
self.consume_token();
}
Some(
Token::DimKeyword
| Token::PrivateKeyword
| Token::PublicKeyword
| Token::ConstKeyword
| Token::StaticKeyword,
) => {
self.parse_dim();
}
_ => {
if self.is_at_label() {
self.parse_label_statement();
} else if self.at_token(Token::LetKeyword)
|| (self.at_token(Token::Whitespace)
&& self.peek_next_keyword() == Some(Token::LetKeyword))
{
self.parse_let_statement();
} else if self.is_at_assignment() {
self.parse_assignment_statement();
} else if self.is_at_procedure_call() {
self.parse_procedure_call();
} else {
self.consume_token_as_unknown();
}
}
}
}
self.builder.finish_node(); }
}
#[cfg(test)]
mod tests {
use super::Parser;
use crate::parsers::cst::{
ControlKind, Creatable, Exposed, FormRoot, NameSpace, ObjectReference, PreDeclaredID,
};
use crate::*;
use assert_matches::assert_matches;
#[test]
fn parse_single_quote_comment() {
let code = "' This is a comment\nSub Main()\n";
let mut source_stream = SourceStream::new("test.bas", code);
let result = tokenize(&mut source_stream);
let (token_stream_opt, _failures) = result.unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let cst = parse(token_stream);
assert_eq!(cst.root_kind(), SyntaxKind::Root);
assert_eq!(cst.child_count(), 3); assert!(cst.text().contains("' This is a comment"));
assert!(cst.text().contains("Sub Main()"));
assert!(cst.contains_kind(SyntaxKind::EndOfLineComment));
assert!(cst.contains_kind(SyntaxKind::SubStatement));
let first = cst.first_child().expect("Expected first child");
assert_eq!(first.kind(), SyntaxKind::EndOfLineComment);
assert!(first.is_token());
}
#[test]
fn syntax_kind_conversions() {
assert_eq!(
SyntaxKind::from(Token::FunctionKeyword),
SyntaxKind::FunctionKeyword
);
assert_eq!(SyntaxKind::from(Token::IfKeyword), SyntaxKind::IfKeyword);
assert_eq!(SyntaxKind::from(Token::ForKeyword), SyntaxKind::ForKeyword);
assert_eq!(
SyntaxKind::from(Token::AdditionOperator),
SyntaxKind::AdditionOperator
);
assert_eq!(
SyntaxKind::from(Token::EqualityOperator),
SyntaxKind::EqualityOperator
);
assert_eq!(
SyntaxKind::from(Token::StringLiteral),
SyntaxKind::StringLiteral
);
assert_eq!(
SyntaxKind::from(Token::IntegerLiteral),
SyntaxKind::IntegerLiteral
);
assert_eq!(
SyntaxKind::from(Token::LongLiteral),
SyntaxKind::LongLiteral
);
assert_eq!(
SyntaxKind::from(Token::SingleLiteral),
SyntaxKind::SingleLiteral
);
assert_eq!(
SyntaxKind::from(Token::DoubleLiteral),
SyntaxKind::DoubleLiteral
);
assert_eq!(
SyntaxKind::from(Token::DateTimeLiteral),
SyntaxKind::DateLiteral
);
}
#[test]
fn parse_empty_stream() {
let source = "";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
assert_eq!(cst.root_kind(), SyntaxKind::Root);
assert_eq!(cst.child_count(), 0);
}
#[test]
fn parse_rem_comment() {
let source = "REM This is a REM comment\nSub Test()\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
assert_eq!(cst.root_kind(), SyntaxKind::Root);
assert_eq!(cst.child_count(), 3); assert!(cst.text().contains("REM This is a REM comment"));
assert!(cst.text().contains("Sub Test()"));
let debug = cst.debug_tree();
assert!(debug.contains("RemComment"));
}
#[test]
fn parse_mixed_comments() {
let source = "' Single quote comment\nREM REM comment\nSub Test()\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
assert_eq!(cst.root_kind(), SyntaxKind::Root);
assert_eq!(cst.child_count(), 5);
assert!(cst.text().contains("' Single quote comment"));
assert!(cst.text().contains("REM REM comment"));
let children = cst.children();
assert_eq!(children[0].kind(), SyntaxKind::EndOfLineComment);
assert_eq!(children[1].kind(), SyntaxKind::Newline);
assert_eq!(children[2].kind(), SyntaxKind::RemComment);
assert_eq!(children[3].kind(), SyntaxKind::Newline);
assert_eq!(children[4].kind(), SyntaxKind::SubStatement);
assert!(cst.contains_kind(SyntaxKind::EndOfLineComment));
assert!(cst.contains_kind(SyntaxKind::RemComment));
}
#[test]
fn cst_with_comments() {
let source = "' This is a comment\nSub Main()\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
assert_eq!(cst.child_count(), 3);
assert!(cst.text().contains("' This is a comment"));
assert!(cst.text().contains("Sub Main()"));
}
#[test]
fn cst_serializable_tree() {
let source = "Sub Test()\nEnd Sub\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
let serializable = cst.to_serializable();
assert_eq!(serializable.root.kind(), SyntaxKind::Root);
assert!(!serializable.root.is_token());
assert_eq!(serializable.root.children().len(), 1);
assert_eq!(
serializable.root.children()[0].kind(),
SyntaxKind::SubStatement
);
}
#[test]
fn cst_serializable_with_insta() {
let source = "Dim x As Integer\n";
let (cst_opt, _failures) = ConcreteSyntaxTree::from_text("test.bas", source).unpack();
let cst = cst_opt.expect("Failed to parse source");
let serializable = cst.to_serializable();
assert!(!serializable.root.children().is_empty());
}
#[test]
fn parser_mode_full_cst_default() {
let source = "Sub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.bas".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let parser = Parser::new(token_stream);
assert_eq!(parser.pos, 0);
}
#[test]
fn parser_mode_direct_extraction() {
let source = "Sub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.bas".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let parser = Parser::new_direct_extraction(tokens, 0);
assert_eq!(parser.pos, 0);
}
#[test]
fn parser_constructors_preserve_tokens() {
let source = "VERSION 5.00\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens_vec = token_stream.into_tokens();
let token_count = tokens_vec.len();
let parser = Parser::new_direct_extraction(tokens_vec, 0);
assert_eq!(parser.tokens.len(), token_count);
assert!(parser.tokens[0].1 == Token::VersionKeyword);
}
#[test]
fn parser_new_with_position() {
let source = "VERSION 5.00\nSub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.bas".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let parser = Parser::new_direct_extraction(tokens, 3);
assert_eq!(parser.pos, 3);
}
#[test]
fn parse_version_direct_with_version() {
let source = "VERSION 5.00\nSub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, failures) = parser.parse_version_direct().unpack();
assert!(version_opt.is_some());
let version = version_opt.expect("Expected version to be parsed");
assert_eq!(version.major, 5);
assert_eq!(version.minor, 0);
assert!(failures.is_empty());
}
#[test]
fn parse_version_direct_without_version() {
let source = "Sub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.bas".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, failures) = parser.parse_version_direct().unpack();
assert!(version_opt.is_none());
assert!(failures.is_empty());
}
#[test]
fn parse_version_direct_with_class_keyword() {
let source = "VERSION 1.0 CLASS\nSub Test()\nEnd Sub\n";
let mut stream = SourceStream::new("test.cls".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, failures) = parser.parse_version_direct().unpack();
assert!(version_opt.is_some());
let version = version_opt.expect("Expected version to be parsed");
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
assert!(failures.is_empty());
}
#[test]
fn parse_version_direct_version_100() {
let source = "VERSION 1.00\n";
let mut stream = SourceStream::new("test.cls".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, _failures) = parser.parse_version_direct().unpack();
assert!(version_opt.is_some());
let version = version_opt.expect("Expected version to be parsed");
assert_eq!(version.major, 1);
assert_eq!(version.minor, 0);
}
#[test]
fn parse_version_direct_with_whitespace() {
let source = " VERSION 5.00 \nSub Test()\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, _failures) = parser.parse_version_direct().unpack();
assert!(version_opt.is_some());
let version = version_opt.expect("Expected version to be parsed");
assert_eq!(version.major, 5);
assert_eq!(version.minor, 0);
}
#[test]
fn parse_version_direct_position_advances() {
let source = "VERSION 5.00\nBegin VB.Form Form1\nEnd\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let initial_pos = parser.pos;
let _result = parser.parse_version_direct();
assert!(parser.pos > initial_pos);
assert_eq!(parser.current_token(), Some(&Token::BeginKeyword));
}
#[test]
fn parse_version_direct_accuracy() {
let test_cases = vec![
("VERSION 5.00\n", Some((5, 0))),
("VERSION 1.0\n", Some((1, 0))),
("VERSION 6.00 CLASS\n", Some((6, 0))),
("VERSION 4.00\n", Some((4, 0))),
("Sub Test()\n", None), ];
for (source, expected) in test_cases {
let mut stream = SourceStream::new("test.vb".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (version_opt, _failures) = parser.parse_version_direct().unpack();
match expected {
Some((major, minor)) => {
assert!(version_opt.is_some(), "Expected version for: {source}");
let version = version_opt.expect("Expected version to be parsed");
assert_eq!(version.major, major, "Major mismatch for: {source}");
assert_eq!(version.minor, minor, "Minor mismatch for: {source}");
}
None => {
assert!(version_opt.is_none(), "Expected no version for: {source}");
}
}
}
}
#[test]
fn parse_control_type_direct_simple() {
let source = "VB.Form Form1\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let control_type = parser.parse_control_type_direct();
assert_eq!(control_type, "VB.Form");
}
#[test]
fn parse_control_name_direct_simple() {
let source = "Form1 \n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let control_name = parser.parse_control_name_direct();
assert_eq!(control_name, "Form1");
}
#[test]
fn parse_property_direct_simple() {
let source = "Caption = \"Hello World\"\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let property = parser.parse_property_direct();
assert!(property.is_some());
let (key, value) = property.expect("Expected property to be parsed");
assert_eq!(key, "Caption");
assert_eq!(value, "\"Hello World\"");
}
#[test]
fn parse_properties_block_to_control_simple_form() {
let source = r#"Begin VB.Form Form1
Caption = "Test Form"
ClientHeight = 3000
ClientWidth = 4000
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_form_root().unpack();
assert!(failures.is_empty(), "Expected no failures");
assert!(control_opt.is_some(), "Expected form root to be parsed");
let form_root = control_opt.expect("Expected form root to be parsed");
assert_eq!(form_root.name(), "Form1");
assert!(form_root.is_form());
}
#[test]
fn parse_properties_block_to_control_command_button() {
let source = r#"Begin VB.CommandButton Command1
Caption = "Click Me"
Height = 495
Width = 1215
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_control().unpack();
assert!(failures.is_empty());
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
assert_eq!(control.name(), "Command1");
assert_matches!(control.kind(), ControlKind::CommandButton { .. });
}
#[test]
fn parse_properties_block_to_control_textbox() {
let source = r#"Begin VB.TextBox Text1
Text = "Initial Text"
Height = 300
Width = 2000
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, _failures) = parser.parse_properties_block_to_control().unpack();
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
assert_eq!(control.name(), "Text1");
assert_matches!(control.kind(), ControlKind::TextBox { .. });
}
#[test]
fn parse_properties_block_without_begin() {
let source = "Caption = \"Test\"\nEnd\n";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, _failures) = parser.parse_properties_block_to_control().unpack();
assert!(control_opt.is_none());
}
#[test]
fn parse_form_with_nested_control() {
let source = r#"Begin VB.Form Form1
Caption = "Main Form"
Begin VB.CommandButton Command1
Caption = "Click Me"
Height = 400
End
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (form_root_opt, failures) = parser.parse_properties_block_to_form_root().unpack();
assert!(failures.is_empty(), "Should have no failures");
assert!(form_root_opt.is_some());
let form_root = form_root_opt.expect("Expected form root to be parsed");
assert_eq!(form_root.name(), "Form1");
if let FormRoot::Form(form) = &form_root {
assert_eq!(form.controls.len(), 1);
assert_eq!(form.controls[0].name(), "Command1");
assert_matches!(form.controls[0].kind(), ControlKind::CommandButton { .. });
} else {
panic!("Expected Form");
}
}
#[test]
fn parse_frame_with_multiple_nested_controls() {
let source = r#"Begin VB.Frame Frame1
Caption = "Options"
Begin VB.CheckBox Check1
Caption = "Option 1"
End
Begin VB.CheckBox Check2
Caption = "Option 2"
End
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_control().unpack();
assert!(failures.is_empty());
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
assert_eq!(control.name(), "Frame1");
if let ControlKind::Frame { controls, .. } = control.kind() {
assert_eq!(controls.len(), 2);
assert_eq!(controls[0].name(), "Check1");
assert_eq!(controls[1].name(), "Check2");
} else {
panic!("Expected Frame control kind");
}
}
#[test]
fn parse_control_with_property_group() {
let source = r#"Begin VB.CommandButton Command1
Caption = "Button"
BeginProperty Font
Name = "Arial"
Size = 12
EndProperty
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_control().unpack();
assert!(failures.is_empty());
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
assert_eq!(control.name(), "Command1");
assert_matches!(control.kind(), ControlKind::CommandButton { .. });
}
#[test]
fn parse_custom_control_with_property_group() {
let source = r#"Begin MSComctlLib.TreeView TreeView1
BeginProperty Font {0BE35203-8F91-11CE-9DE3-00AA004BB851}
Name = "MS Sans Serif"
Size = 8.25
Charset = 0
EndProperty
Caption = "Tree"
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_control().unpack();
assert!(failures.is_empty());
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
assert_eq!(control.name(), "TreeView1");
if let ControlKind::Custom {
property_groups, ..
} = control.kind()
{
assert_eq!(property_groups.len(), 1);
assert_eq!(property_groups[0].name, "Font");
assert!(property_groups[0].guid.is_some());
} else {
panic!("Expected Custom control kind");
}
}
#[test]
fn parse_simple_object_statement() {
let source = r#"Object = "{12345678-1234-1234-1234-123456789ABC}#1.0#0"; "MyLib.dll""#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let objects = parser.parse_objects_direct();
assert_eq!(objects.len(), 1);
match &objects[0] {
ObjectReference::Compiled {
uuid,
version,
unknown1,
file_name,
} => {
assert_eq!(
uuid.to_string().to_uppercase(),
"12345678-1234-1234-1234-123456789ABC"
);
assert_eq!(version, "1.0");
assert_eq!(unknown1, "0");
assert_eq!(file_name, "MyLib.dll");
}
ObjectReference::Project { .. } => {
panic!("Expected Compiled object reference")
}
}
}
#[test]
fn parse_multiple_object_statements() {
let source = r#"Object = "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}#1.0#0"; "Lib1.dll"
Object = "{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}#2.0#1"; "Lib2.ocx"
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let objects = parser.parse_objects_direct();
assert_eq!(objects.len(), 2);
match &objects[0] {
ObjectReference::Compiled { file_name, .. } => {
assert_eq!(file_name, "Lib1.dll");
}
ObjectReference::Project { .. } => {
panic!("Expected Compiled object reference")
}
}
match &objects[1] {
ObjectReference::Compiled { file_name, .. } => {
assert_eq!(file_name, "Lib2.ocx");
}
ObjectReference::Project { .. } => {
panic!("Expected Compiled object reference")
}
}
}
#[test]
fn parse_embedded_object_statement() {
let source = r#"Object = *\G{87654321-4321-4321-4321-CBA987654321}#3.0#5; "Embedded.ocx""#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let objects = parser.parse_objects_direct();
assert_eq!(objects.len(), 1);
match &objects[0] {
ObjectReference::Compiled {
uuid,
version,
file_name,
..
} => {
assert_eq!(
uuid.to_string().to_uppercase(),
"87654321-4321-4321-4321-CBA987654321"
);
assert_eq!(version, "3.0");
assert_eq!(file_name, "Embedded.ocx");
}
ObjectReference::Project { .. } => {
panic!("Expected Compiled object reference")
}
}
}
#[test]
fn parse_nested_property_groups() {
use either::Either;
let source = r#"Begin Custom.Control Ctrl1
BeginProperty Outer
Value1 = "Test"
BeginProperty Inner
Value2 = "Nested"
EndProperty
EndProperty
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (control_opt, failures) = parser.parse_properties_block_to_control().unpack();
assert!(failures.is_empty());
assert!(control_opt.is_some());
let control = control_opt.expect("Expected control to be parsed");
if let ControlKind::Custom {
property_groups, ..
} = control.kind()
{
assert_eq!(property_groups.len(), 1);
assert_eq!(property_groups[0].name, "Outer");
if let Some(Either::Right(inner)) = property_groups[0].properties.get("Inner") {
assert_eq!(inner.name, "Inner");
} else {
panic!("Expected nested Inner property group");
}
} else {
panic!("Expected Custom control kind");
}
}
#[test]
fn parse_deeply_nested_controls() {
let source = r#"Begin VB.Form Form1
Caption = "Outer"
Begin VB.PictureBox Picture1
Begin VB.Frame Frame1
Begin VB.Label Label1
Caption = "Deep"
End
End
End
End
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let (form_root_opt, failures) = parser.parse_properties_block_to_form_root().unpack();
assert!(failures.is_empty());
assert!(form_root_opt.is_some());
let form_root = form_root_opt.expect("Expected form root to be parsed");
if let FormRoot::Form(form) = &form_root {
assert_eq!(form.controls.len(), 1);
if let ControlKind::PictureBox { controls, .. } = form.controls[0].kind() {
assert_eq!(controls.len(), 1);
if let ControlKind::Frame { controls, .. } = controls[0].kind() {
assert_eq!(controls.len(), 1);
assert_eq!(controls[0].name(), "Label1");
} else {
panic!("Expected Frame");
}
} else {
panic!("Expected PictureBox");
}
} else {
panic!("Expected Form");
}
}
#[test]
fn parse_simple_string_attribute() {
let source = r#"Attribute VB_Name = "Form1"
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.name, "Form1");
assert_eq!(attrs.global_name_space, NameSpace::Local);
assert_eq!(attrs.creatable, Creatable::True);
assert_eq!(attrs.predeclared_id, PreDeclaredID::False);
assert_eq!(attrs.exposed, Exposed::False);
assert_eq!(attrs.description, None);
}
#[test]
fn parse_boolean_attributes() {
let source = r"Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.global_name_space, NameSpace::Local);
assert_eq!(attrs.creatable, Creatable::True);
assert_eq!(attrs.predeclared_id, PreDeclaredID::True);
assert_eq!(attrs.exposed, Exposed::False);
}
#[test]
fn parse_numeric_attribute() {
let source = r"Attribute VB_PredeclaredId = -1
";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.predeclared_id, PreDeclaredID::True);
}
#[test]
fn parse_multiple_attributes() {
let source = r#"Attribute VB_Name = "MyForm"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Attribute VB_Description = "This is a test form"
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.name, "MyForm");
assert_eq!(attrs.global_name_space, NameSpace::Local);
assert_eq!(attrs.creatable, Creatable::False);
assert_eq!(attrs.predeclared_id, PreDeclaredID::True);
assert_eq!(attrs.exposed, Exposed::False);
assert_eq!(attrs.description, Some("This is a test form".to_string()));
}
#[test]
fn parse_ext_key_attributes() {
let source = r#"Attribute VB_Name = "Form1"
Attribute VB_Ext_KEY = "CustomKey" ,"CustomValue"
Attribute VB_Description = "Test"
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.name, "Form1");
assert_eq!(attrs.description, Some("Test".to_string()));
assert_eq!(attrs.ext_key.len(), 1);
assert!(attrs.ext_key.contains_key("VB_Ext_KEY"));
}
#[test]
fn parse_empty_attributes() {
let source = r"";
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let attrs = parser.parse_attributes_direct();
assert_eq!(attrs.name, "");
assert_eq!(attrs.global_name_space, NameSpace::Local);
assert_eq!(attrs.creatable, Creatable::True);
assert_eq!(attrs.predeclared_id, PreDeclaredID::False);
assert_eq!(attrs.exposed, Exposed::False);
assert_eq!(attrs.description, None);
assert!(attrs.ext_key.is_empty());
}
#[test]
fn parse_resource_reference_property() {
let source = r#"Caption = $"Gradient.frx":0000
"#;
let mut stream = SourceStream::new("test.frm".to_string(), source);
let (token_stream_opt, _) = tokenize(&mut stream).unpack();
let token_stream = token_stream_opt.expect("Tokenization failed");
let tokens = token_stream.into_tokens();
let mut parser = Parser::new_direct_extraction(tokens, 0);
let property = parser.parse_property_direct();
assert!(property.is_some());
let (key, value) = property.expect("Expected property to be parsed");
assert_eq!(key, "Caption");
assert_eq!(value, r#"$"Gradient.frx":0000"#);
}
}