use crate::error::{Result, SemanticError, SourceLocation};
use crate::scope::{ScopeKind, ScopeManager};
use crate::symbols::{Symbol, SymbolKind, Visibility};
use crate::types::{TypeInfo, VBType};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::path::Path;
use vb6parse::files::project::ProjectReference;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ReferenceInfo {
pub guid: Option<String>,
pub path: String,
pub description: String,
pub is_subproject: bool,
}
impl ReferenceInfo {
pub fn from_project_reference(reference: &ProjectReference) -> ReferenceInfo {
match reference {
ProjectReference::Compiled {
uuid,
path,
description,
..
} => ReferenceInfo {
guid: Some(uuid.to_string()),
path: (*path).to_string(),
description: (*description).to_string(),
is_subproject: false,
},
ProjectReference::SubProject { path } => ReferenceInfo {
guid: None,
path: (*path).to_string(),
description: String::new(),
is_subproject: true,
},
}
}
pub fn display_name(&self) -> String {
if !self.description.is_empty() {
self.description.clone()
} else if let Some(guid) = &self.guid {
guid.clone()
} else {
self.path.clone()
}
}
pub fn matches_key(key: &str, reference: &ReferenceInfo) -> bool {
fn normalize(value: &str) -> String {
value
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.to_ascii_lowercase()
}
fn path_stem(path: &str) -> Option<String> {
let file_name = path.rsplit(['/', '\\']).next()?;
if file_name.is_empty() {
return None;
}
let stem = file_name
.rsplit_once('.')
.map(|(stem, _extension)| stem)
.unwrap_or(file_name);
Some(stem.to_string())
}
let key = normalize(key);
if normalize(&reference.description) == key {
return true;
}
if reference
.guid
.as_ref()
.is_some_and(|guid| normalize(guid) == key)
{
return true;
}
if normalize(&reference.path) == key {
return true;
}
if reference
.path
.rsplit(['/', '\\'])
.next()
.filter(|file_name| !file_name.is_empty())
.is_some_and(|file_name| normalize(file_name) == key)
{
return true;
}
path_stem(&reference.path).is_some_and(|stem| normalize(&stem) == key)
}
}
impl fmt::Display for ReferenceInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.display_name())
}
}
pub struct ReferenceContext<'a> {
scope_manager: &'a mut ScopeManager,
file: &'a str,
}
impl<'a> ReferenceContext<'a> {
pub(crate) fn new(scope_manager: &'a mut ScopeManager, file: &'a str) -> Self {
Self {
scope_manager,
file,
}
}
pub fn file(&self) -> &str {
self.file
}
pub fn current_scope_id(&self) -> usize {
self.scope_manager.current_scope_id()
}
pub fn scope_manager(&mut self) -> &mut ScopeManager {
self.scope_manager
}
pub fn push_scope(&mut self, kind: ScopeKind, name: String) -> usize {
match kind {
ScopeKind::Reference => self.scope_manager.push_reference_scope(name),
_ => self.scope_manager.push_scope(kind, name),
}
}
pub fn push_reference_scope(&mut self, name: String) -> usize {
self.scope_manager.push_reference_scope(name)
}
pub fn pop_scope(&mut self) -> Result<()> {
self.scope_manager.pop_scope()
}
pub fn add_symbol(&mut self, mut symbol: Symbol) -> Result<()> {
symbol.scope_id = self.scope_manager.current_scope_id();
match self.scope_manager.add_symbol(symbol) {
Ok(()) => Ok(()),
Err(SemanticError::DuplicateSymbol { .. }) => Ok(()),
Err(other) => Err(other),
}
}
}
pub trait ReferenceResolver {
fn name(&self) -> &str;
fn can_handle(&self, reference: &ReferenceInfo) -> bool;
fn resolve(&mut self, reference: &ReferenceInfo, context: &mut ReferenceContext) -> Result<()>;
}
#[derive(Default)]
pub struct ReferenceRegistry {
resolvers: Vec<Box<dyn ReferenceResolver>>,
}
impl ReferenceRegistry {
pub fn new() -> Self {
Self {
resolvers: Vec::new(),
}
}
pub fn register(&mut self, resolver: Box<dyn ReferenceResolver>) {
self.resolvers.push(resolver);
}
pub fn resolvers(&self) -> &[Box<dyn ReferenceResolver>] {
&self.resolvers
}
pub fn is_empty(&self) -> bool {
self.resolvers.is_empty()
}
pub fn resolve(
&mut self,
reference: &ReferenceInfo,
scopes: &mut ScopeManager,
file: &str,
) -> Result<bool> {
for resolver in &mut self.resolvers {
if !resolver.can_handle(reference) {
continue;
}
let mut context = ReferenceContext::new(scopes, file);
resolver.resolve(reference, &mut context)?;
return Ok(true);
}
Ok(false)
}
}
pub struct StaticReferenceResolver {
name: String,
keys: Vec<String>,
symbols: Vec<Symbol>,
}
impl StaticReferenceResolver {
pub fn new(name: impl Into<String>, keys: Vec<String>, symbols: Vec<Symbol>) -> Self {
Self {
name: name.into(),
keys,
symbols,
}
}
}
impl ReferenceResolver for StaticReferenceResolver {
fn name(&self) -> &str {
&self.name
}
fn can_handle(&self, reference: &ReferenceInfo) -> bool {
self.keys
.iter()
.any(|key| ReferenceInfo::matches_key(key, reference))
}
fn resolve(&mut self, reference: &ReferenceInfo, context: &mut ReferenceContext) -> Result<()> {
context.push_reference_scope(reference.display_name());
for symbol in &self.symbols {
context.add_symbol(symbol.clone())?;
}
context.pop_scope()
}
}
pub struct ManifestReferenceResolver {
entries: BTreeMap<String, Vec<ManifestSymbol>>,
}
impl ManifestReferenceResolver {
pub fn from_json(json: &str) -> Result<Self> {
let manifest: Manifest = serde_json::from_str(json).map_err(|error| {
SemanticError::AnalysisError(format!("Failed to parse reference manifest: {error}"))
})?;
Ok(Self {
entries: manifest.references,
})
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
let contents = std::fs::read_to_string(path.as_ref()).map_err(|error| {
SemanticError::FileReadError {
file: path.as_ref().display().to_string(),
message: error.to_string(),
}
})?;
Self::from_json(&contents)
}
}
impl ReferenceResolver for ManifestReferenceResolver {
fn name(&self) -> &str {
"manifest"
}
fn can_handle(&self, reference: &ReferenceInfo) -> bool {
self.entries
.keys()
.any(|key| ReferenceInfo::matches_key(key, reference))
}
fn resolve(&mut self, reference: &ReferenceInfo, context: &mut ReferenceContext) -> Result<()> {
let Some((_key, specs)) = self
.entries
.iter()
.find(|(key, _)| ReferenceInfo::matches_key(key, reference))
else {
return Err(SemanticError::AnalysisError(format!(
"Reference resolver 'manifest' has no symbols for reference '{}'",
reference.display_name()
)));
};
context.push_reference_scope(reference.display_name());
for spec in specs {
context.add_symbol(spec.to_symbol(&SourceLocation {
file: context.file().to_string(),
line: 1,
column: 1,
}))?;
}
context.pop_scope()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct Manifest {
pub references: BTreeMap<String, Vec<ManifestSymbol>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManifestSymbol {
pub name: String,
#[serde(default)]
pub kind: ManifestSymbolKind,
#[serde(default = "ManifestSymbol::default_type_name")]
pub r#type: String,
#[serde(default)]
pub is_array: bool,
#[serde(default)]
pub visibility: ManifestVisibility,
}
impl ManifestSymbol {
fn default_type_name() -> String {
"Variant".to_string()
}
fn to_symbol(&self, location: &SourceLocation) -> Symbol {
let mut type_info = TypeInfo::new(type_kind_from_name(&self.r#type));
type_info.is_array = self.is_array;
Symbol {
name: self.name.clone(),
kind: self.kind.into(),
type_info,
visibility: self.visibility.into(),
location: location.clone(),
scope_id: 0,
attributes: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManifestSymbolKind {
Constant,
#[default]
Variable,
SubProcedure,
Function,
PropertyGet,
PropertyLet,
PropertySet,
Class,
Module,
Form,
Control,
Enum,
EnumMember,
UserType,
TypeMember,
Parameter,
Label,
}
impl From<ManifestSymbolKind> for SymbolKind {
fn from(kind: ManifestSymbolKind) -> Self {
match kind {
ManifestSymbolKind::Constant => SymbolKind::Constant,
ManifestSymbolKind::Variable => SymbolKind::Variable,
ManifestSymbolKind::SubProcedure => SymbolKind::SubProcedure,
ManifestSymbolKind::Function => SymbolKind::Function,
ManifestSymbolKind::PropertyGet => SymbolKind::PropertyGet,
ManifestSymbolKind::PropertyLet => SymbolKind::PropertyLet,
ManifestSymbolKind::PropertySet => SymbolKind::PropertySet,
ManifestSymbolKind::Class => SymbolKind::Class,
ManifestSymbolKind::Module => SymbolKind::Module,
ManifestSymbolKind::Form => SymbolKind::Form,
ManifestSymbolKind::Control => SymbolKind::Control,
ManifestSymbolKind::Enum => SymbolKind::Enum,
ManifestSymbolKind::EnumMember => SymbolKind::EnumMember,
ManifestSymbolKind::UserType => SymbolKind::UserType,
ManifestSymbolKind::TypeMember => SymbolKind::TypeMember,
ManifestSymbolKind::Parameter => SymbolKind::Parameter,
ManifestSymbolKind::Label => SymbolKind::Label,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ManifestVisibility {
#[default]
Public,
Private,
Friend,
Global,
}
impl From<ManifestVisibility> for Visibility {
fn from(visibility: ManifestVisibility) -> Self {
match visibility {
ManifestVisibility::Public => Visibility::Public,
ManifestVisibility::Private => Visibility::Private,
ManifestVisibility::Friend => Visibility::Friend,
ManifestVisibility::Global => Visibility::Global,
}
}
}
fn type_kind_from_name(name: &str) -> VBType {
match name.to_ascii_lowercase().as_str() {
"integer" => VBType::Integer,
"long" => VBType::Long,
"single" => VBType::Single,
"double" => VBType::Double,
"currency" => VBType::Currency,
"string" => VBType::String,
"boolean" => VBType::Boolean,
"byte" => VBType::Byte,
"date" => VBType::Date,
"variant" => VBType::Variant,
"object" => VBType::Object,
"nothing" => VBType::Nothing,
_ => VBType::Class(name.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::SourceLocation;
fn location() -> SourceLocation {
SourceLocation {
file: "<test>".to_string(),
line: 1,
column: 1,
}
}
fn symbol(name: &str) -> Symbol {
Symbol {
name: name.to_string(),
kind: SymbolKind::Constant,
type_info: TypeInfo::new(VBType::String),
visibility: Visibility::Public,
location: location(),
scope_id: 0,
attributes: HashMap::new(),
}
}
fn reference_info() -> ReferenceInfo {
ReferenceInfo {
guid: Some("00020430-0000-0000-C000-000000000046".to_string()),
path: "C:\\Windows\\System32\\stdole2.tlb".to_string(),
description: "OLE Automation".to_string(),
is_subproject: false,
}
}
#[test]
fn reference_info_matches_key() {
let info = reference_info();
assert!(ReferenceInfo::matches_key("OLE Automation", &info));
assert!(ReferenceInfo::matches_key("ole automation", &info));
assert!(ReferenceInfo::matches_key(
"{00020430-0000-0000-C000-000000000046}",
&info
));
assert!(ReferenceInfo::matches_key(
"00020430-0000-0000-c000-000000000046",
&info
));
assert!(ReferenceInfo::matches_key(
"C:\\Windows\\System32\\stdole2.tlb",
&info
));
assert!(ReferenceInfo::matches_key("stdole2.tlb", &info));
assert!(ReferenceInfo::matches_key("stdole2", &info));
assert!(!ReferenceInfo::matches_key("Nope", &info));
}
#[test]
fn unhandled_reference_returns_false() {
let mut registry = ReferenceRegistry::new();
let mut scopes = ScopeManager::new();
assert!(
!registry
.resolve(&reference_info(), &mut scopes, "test.vbp")
.unwrap()
);
}
#[test]
fn static_resolver_populates_reference_scope() {
let mut registry = ReferenceRegistry::new();
registry.register(Box::new(StaticReferenceResolver::new(
"test",
vec!["OLE Automation".to_string()],
vec![symbol("Now")],
)));
let mut scopes = ScopeManager::new();
let handled = registry
.resolve(&reference_info(), &mut scopes, "test.vbp")
.unwrap();
assert!(handled);
let reference_scopes = scopes.get_scopes_by_kind(ScopeKind::Reference);
assert_eq!(reference_scopes.len(), 1);
assert!(reference_scopes[0].symbols.contains_key("Now"));
}
#[test]
fn manifest_resolver_loads_symbols_from_json() {
let json = r#"{
"references": {
"OLE Automation": [
{ "name": "Now", "kind": "function", "type": "Date" },
{ "name": "vbCrLf", "kind": "constant", "type": "String" },
{ "name": "items", "kind": "variable", "type": "Long", "is_array": true }
]
}
}"#;
let mut registry = ReferenceRegistry::new();
registry.register(Box::new(
ManifestReferenceResolver::from_json(json).unwrap(),
));
let mut scopes = ScopeManager::new();
let handled = registry
.resolve(&reference_info(), &mut scopes, "test.vbp")
.unwrap();
assert!(handled);
let scope = &scopes.get_scopes_by_kind(ScopeKind::Reference)[0];
let now = &scope.symbols["Now"];
assert_eq!(now.kind, SymbolKind::Function);
assert_eq!(now.type_info.kind, VBType::Date);
let crlf = &scope.symbols["vbCrLf"];
assert_eq!(crlf.kind, SymbolKind::Constant);
let items = &scope.symbols["items"];
assert!(items.type_info.is_array);
assert_eq!(items.type_info.kind, VBType::Long);
}
}