use crate::analyzer::SemanticAnalyzer;
use crate::error::{SemanticError, SourceLocation};
use crate::query::QueryIndex;
use crate::scope::{Scope, ScopeManager};
use crate::symbols::Symbol;
use crate::types::TypeInfo;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
use vb6parse::io::SourceFile;
use wasm_bindgen::prelude::*;
#[derive(Serialize, Deserialize)]
pub struct LocationInfo {
pub file: String,
pub line: usize,
pub column: usize,
}
#[derive(Serialize, Deserialize)]
pub struct SemanticErrorInfo {
#[serde(rename = "type")]
pub type_name: String,
pub message: String,
pub location: Option<LocationInfo>,
}
#[derive(Serialize, Deserialize)]
pub struct SymbolInfo {
pub name: String,
pub kind: String,
pub type_info: TypeInfo,
pub type_display: String,
pub visibility: String,
pub location: LocationInfo,
pub scope_id: usize,
pub attributes: Vec<(String, String)>,
}
#[derive(Serialize, Deserialize)]
pub struct ScopeInfo {
pub id: usize,
pub kind: String,
pub parent: Option<usize>,
pub children: Vec<usize>,
pub name: String,
pub symbols: Vec<SymbolInfo>,
}
#[derive(Serialize, Deserialize)]
pub struct WasmReferenceInfo {
pub guid: Option<String>,
pub path: String,
pub description: String,
pub is_subproject: bool,
pub display_name: String,
}
#[derive(Serialize, Deserialize)]
pub struct WasmSymbolReference {
pub scope_id: usize,
pub name: String,
pub kind: String,
pub location: LocationInfo,
pub start_offset: u32,
pub end_offset: u32,
pub end_column: usize,
}
#[derive(Serialize, Deserialize)]
pub struct AnalysisOutput {
pub scopes: Vec<ScopeInfo>,
pub errors: Vec<SemanticErrorInfo>,
pub warnings: Vec<String>,
pub resolved_references: Vec<WasmReferenceInfo>,
pub unresolved_references: Vec<WasmReferenceInfo>,
pub references: Vec<WasmSymbolReference>,
pub successful: bool,
pub error_count: usize,
pub warning_count: usize,
pub symbol_count: usize,
pub scope_count: usize,
pub analyze_time_ms: f64,
}
fn convert_location(location: &SourceLocation) -> LocationInfo {
LocationInfo {
file: location.file.clone(),
line: location.line,
column: location.column,
}
}
fn convert_symbol(symbol: &Symbol) -> SymbolInfo {
let mut attributes: Vec<(String, String)> = symbol
.attributes
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
attributes.sort();
SymbolInfo {
name: symbol.name.clone(),
kind: format!("{:?}", symbol.kind),
type_info: symbol.type_info.clone(),
type_display: symbol.type_info.to_string(),
visibility: format!("{:?}", symbol.visibility),
location: convert_location(&symbol.location),
scope_id: symbol.scope_id,
attributes,
}
}
fn convert_scope(scope: &Scope) -> ScopeInfo {
let mut symbols: Vec<SymbolInfo> = scope.symbols.values().map(convert_symbol).collect();
symbols.sort_by(|a, b| a.name.cmp(&b.name));
ScopeInfo {
id: scope.id,
kind: format!("{:?}", scope.kind),
parent: scope.parent,
children: scope.children.clone(),
name: scope.name.clone(),
symbols,
}
}
fn convert_scope_manager(scope_manager: &ScopeManager) -> Vec<ScopeInfo> {
let mut scopes: Vec<ScopeInfo> = scope_manager
.all_scopes()
.into_iter()
.map(convert_scope)
.collect();
scopes.sort_by_key(|scope| scope.id);
scopes
}
fn error_type_name(error: &SemanticError) -> &'static str {
match error {
SemanticError::UndefinedSymbol { .. } => "UndefinedSymbol",
SemanticError::DuplicateSymbol { .. } => "DuplicateSymbol",
SemanticError::TypeMismatch { .. } => "TypeMismatch",
SemanticError::InvalidScope { .. } => "InvalidScope",
SemanticError::InvalidType { .. } => "InvalidType",
SemanticError::CircularDependency { .. } => "CircularDependency",
SemanticError::InvalidOperation { .. } => "InvalidOperation",
SemanticError::InaccessibleSymbol { .. } => "InaccessibleSymbol",
SemanticError::InvalidAssignment { .. } => "InvalidAssignment",
SemanticError::ParameterMismatch { .. } => "ParameterMismatch",
SemanticError::FileReadError { .. } => "FileReadError",
SemanticError::FileParseError { .. } => "FileParseError",
SemanticError::AnalysisError(_) => "AnalysisError",
}
}
fn error_location(error: &SemanticError) -> Option<LocationInfo> {
match error {
SemanticError::UndefinedSymbol { location, .. }
| SemanticError::DuplicateSymbol { location, .. }
| SemanticError::TypeMismatch { location, .. }
| SemanticError::InvalidType { location, .. }
| SemanticError::InvalidOperation { location, .. }
| SemanticError::InaccessibleSymbol { location, .. }
| SemanticError::InvalidAssignment { location, .. }
| SemanticError::ParameterMismatch { location, .. } => Some(convert_location(location)),
_ => None,
}
}
fn convert_error(error: &SemanticError) -> SemanticErrorInfo {
SemanticErrorInfo {
type_name: error_type_name(error).to_string(),
message: error.to_string(),
location: error_location(error),
}
}
fn build_analysis_output(analyzer: &SemanticAnalyzer) -> AnalysisOutput {
let scopes = convert_scope_manager(analyzer.scope_manager());
let errors: Vec<SemanticErrorInfo> = analyzer.errors().iter().map(convert_error).collect();
let warnings = analyzer.warnings().to_vec();
let references = convert_query_index(analyzer.query_index());
AnalysisOutput {
successful: errors.is_empty(),
error_count: errors.len(),
warning_count: warnings.len(),
symbol_count: scopes.iter().map(|scope| scope.symbols.len()).sum(),
scope_count: scopes.len(),
scopes,
errors,
warnings,
resolved_references: Vec::new(),
unresolved_references: Vec::new(),
references,
analyze_time_ms: 0.0,
}
}
fn convert_query_index(index: &QueryIndex) -> Vec<WasmSymbolReference> {
index
.iter()
.flat_map(|(key, references)| {
references.iter().map(move |reference| WasmSymbolReference {
scope_id: key.scope_id,
name: key.name.clone(),
kind: format!("{:?}", reference.kind),
location: convert_location(&reference.location),
start_offset: reference.start_offset,
end_offset: reference.end_offset,
end_column: reference.end_column,
})
})
.collect()
}
fn analyze_source(
analyzer: &mut SemanticAnalyzer,
source: &SourceFile,
file_type: &str,
) -> Result<(), JsError> {
match file_type {
"module" | "bas" => {
let (module_opt, _failures) = vb6parse::files::ModuleFile::parse(source).unpack();
let Some(module) = module_opt else {
return Err(JsError::new(
"Failed to parse the input code as a VB6 module (.bas).",
));
};
analyzer
.analyze_module(&module)
.map_err(|e| JsError::new(&e.to_string()))?;
}
"class" | "cls" => {
let (class_opt, _failures) = vb6parse::files::ClassFile::parse(source).unpack();
let Some(class) = class_opt else {
return Err(JsError::new(
"Failed to parse the input code as a VB6 class (.cls).",
));
};
analyzer
.analyze_class(&class)
.map_err(|e| JsError::new(&e.to_string()))?;
}
"form" | "frm" => {
let (form_opt, _failures) = vb6parse::files::FormFile::parse(source).unpack();
let Some(form) = form_opt else {
return Err(JsError::new(
"Failed to parse the input code as a VB6 form (.frm).",
));
};
analyzer
.analyze_form(&form)
.map_err(|e| JsError::new(&e.to_string()))?;
}
other => {
return Err(JsError::new(&format!(
"Unknown file type '{other}'. Supported types are 'module', 'class', 'form'."
)));
}
}
Ok(())
}
#[wasm_bindgen]
pub fn analyze_vb6_code(code: &str, file_type: &str) -> Result<JsValue, JsError> {
let source = SourceFile::from_string("test.bas", code);
let mut analyzer = SemanticAnalyzer::new();
analyze_source(&mut analyzer, &source, file_type)?;
let output = build_analysis_output(&analyzer);
Ok(to_value(&output).unwrap())
}