use vize_carton::{CompactString, FxHashMap, String};
#[derive(Debug, Clone)]
pub struct ResolvedType {
pub raw: CompactString,
pub is_reference: bool,
pub body: Option<CompactString>,
}
#[derive(Debug, Clone)]
pub struct TypeProperty {
pub name: CompactString,
pub prop_type: Option<CompactString>,
pub optional: bool,
}
#[derive(Debug, Default)]
pub struct TypeDefinitions {
pub interfaces: FxHashMap<CompactString, CompactString>,
pub type_aliases: FxHashMap<CompactString, CompactString>,
pub imported_types: FxHashMap<CompactString, CompactString>,
}
impl TypeDefinitions {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn add_interface(
&mut self,
name: impl Into<CompactString>,
body: impl Into<CompactString>,
) {
self.interfaces.insert(name.into(), body.into());
}
#[inline]
pub fn add_type_alias(
&mut self,
name: impl Into<CompactString>,
body: impl Into<CompactString>,
) {
self.type_aliases.insert(name.into(), body.into());
}
#[inline]
pub fn add_imported_type(
&mut self,
name: impl Into<CompactString>,
source: impl Into<CompactString>,
) {
self.imported_types.insert(name.into(), source.into());
}
pub fn resolve(&self, type_name: &str) -> Option<&CompactString> {
self.interfaces
.get(type_name)
.or_else(|| self.type_aliases.get(type_name))
}
#[inline]
pub fn is_defined(&self, type_name: &str) -> bool {
self.interfaces.contains_key(type_name) || self.type_aliases.contains_key(type_name)
}
#[inline]
pub fn is_imported(&self, type_name: &str) -> bool {
self.imported_types.contains_key(type_name)
}
}
#[derive(Debug, Default)]
pub struct TypeResolver {
definitions: TypeDefinitions,
}
impl TypeResolver {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn definitions(&self) -> &TypeDefinitions {
&self.definitions
}
#[inline]
pub fn definitions_mut(&mut self) -> &mut TypeDefinitions {
&mut self.definitions
}
#[inline]
pub fn add_interface(
&mut self,
name: impl Into<CompactString>,
body: impl Into<CompactString>,
) {
self.definitions.add_interface(name, body);
}
#[inline]
pub fn add_type_alias(
&mut self,
name: impl Into<CompactString>,
body: impl Into<CompactString>,
) {
self.definitions.add_type_alias(name, body);
}
pub fn extract_properties(&self, type_args: &str) -> Vec<TypeProperty> {
let content = type_args.trim();
let resolved_content = if content.starts_with('{') {
if content.ends_with('}') {
&content[1..content.len() - 1]
} else {
content
}
} else {
if let Some(body) = self.definitions.resolve(content) {
let body = body.trim();
if body.starts_with('{') && body.ends_with('}') {
&body[1..body.len() - 1]
} else {
body
}
} else {
return Vec::new();
}
};
self.parse_type_members(resolved_content)
}
fn parse_type_members(&self, content: &str) -> Vec<TypeProperty> {
let mut properties = Vec::new();
let mut depth = 0;
let mut current = String::default();
for c in content.chars() {
match c {
'{' | '<' | '(' | '[' => {
depth += 1;
current.push(c);
}
'}' | '>' | ')' | ']' => {
depth -= 1;
current.push(c);
}
',' | ';' | '\n' if depth == 0 => {
if let Some(prop) = self.parse_single_property(¤t) {
properties.push(prop);
}
current.clear();
}
_ => current.push(c),
}
}
if let Some(prop) = self.parse_single_property(¤t) {
properties.push(prop);
}
properties
}
fn parse_single_property(&self, segment: &str) -> Option<TypeProperty> {
let trimmed = segment.trim();
if trimmed.is_empty() {
return None;
}
let colon_pos = trimmed.find(':')?;
let name_part = &trimmed[..colon_pos];
let type_part = &trimmed[colon_pos + 1..];
let optional = name_part.ends_with('?');
let name = name_part.trim().trim_end_matches('?').trim();
if name.is_empty() || !is_valid_identifier(name) {
return None;
}
Some(TypeProperty {
name: CompactString::new(name),
prop_type: Some(CompactString::new(type_part.trim())),
optional,
})
}
pub fn extract_emits(&self, type_args: &str) -> Vec<CompactString> {
let content = type_args.trim();
let mut emits = Vec::new();
let resolved = if content.starts_with('{') {
if content.ends_with('}') {
&content[1..content.len() - 1]
} else {
content
}
} else if let Some(body) = self.definitions.resolve(content) {
let body = body.trim();
if body.starts_with('{') && body.ends_with('}') {
&body[1..body.len() - 1]
} else {
body
}
} else {
return emits;
};
for segment in resolved.split(&[';', '\n'][..]) {
let trimmed = segment.trim();
if trimmed.starts_with('(') {
if let Some(event_name) = extract_event_from_call_signature(trimmed) {
emits.push(event_name);
}
}
else if !trimmed.is_empty() {
for prop in trimmed.split(',') {
let prop = prop.trim();
if let Some(colon_pos) = prop.find(':') {
let name = prop[..colon_pos].trim();
if !name.is_empty() && is_valid_identifier(name) {
emits.push(CompactString::new(name));
}
}
}
}
}
emits
}
}
fn extract_event_from_call_signature(signature: &str) -> Option<CompactString> {
let colon_pos = signature.find(':')?;
let after_colon = &signature[colon_pos + 1..];
let quote_char = if after_colon.contains('\'') {
'\''
} else if after_colon.contains('"') {
'"'
} else {
return None;
};
let start = after_colon.find(quote_char)? + 1;
let rest = &after_colon[start..];
let end = rest.find(quote_char)?;
Some(CompactString::new(&rest[..end]))
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_ascii_alphabetic() && first != '_' && first != '$' {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}
#[cfg(test)]
mod tests {
use super::{TypeDefinitions, TypeResolver};
#[test]
fn test_extract_inline_props() {
let resolver = TypeResolver::new();
let props = resolver.extract_properties("{ msg: string, count?: number }");
assert_eq!(props.len(), 2);
assert_eq!(props[0].name.as_str(), "msg");
assert!(!props[0].optional);
assert_eq!(props[1].name.as_str(), "count");
assert!(props[1].optional);
}
#[test]
fn test_extract_props_from_reference() {
let mut resolver = TypeResolver::new();
resolver.add_interface("Props", "{ foo: string; bar: number }");
let props = resolver.extract_properties("Props");
assert_eq!(props.len(), 2);
assert_eq!(props[0].name.as_str(), "foo");
assert_eq!(props[1].name.as_str(), "bar");
}
#[test]
fn test_extract_emits_call_signature() {
let resolver = TypeResolver::new();
let emits =
resolver.extract_emits("{ (e: 'click'): void; (e: 'update', value: number): void }");
assert_eq!(emits.len(), 2);
assert_eq!(emits[0].as_str(), "click");
assert_eq!(emits[1].as_str(), "update");
}
#[test]
fn test_extract_emits_object_type() {
let resolver = TypeResolver::new();
let emits = resolver.extract_emits("{ click: []; update: [value: number] }");
assert_eq!(emits.len(), 2);
assert_eq!(emits[0].as_str(), "click");
assert_eq!(emits[1].as_str(), "update");
}
#[test]
fn test_type_definitions() {
let mut defs = TypeDefinitions::new();
defs.add_interface("Props", "{ msg: string }");
defs.add_type_alias("Count", "number");
assert!(defs.is_defined("Props"));
assert!(defs.is_defined("Count"));
assert!(!defs.is_defined("Unknown"));
assert!(defs.resolve("Props").is_some());
assert!(defs.resolve("Count").is_some());
}
}