use super::*;
use std::collections::{BTreeMap};
use std::collections::HashMap;
use std::collections::HashSet;
#[derive(Debug)]
pub struct SymbolResolver {
symbol_table: HashMap<String, SymbolInfo>,
import_graph: HashMap<String, HashSet<String>>,
config: ResolverConfig,
}
impl SymbolResolver {
pub fn new() -> Self {
Self {
symbol_table: HashMap::new(),
import_graph: HashMap::new(),
config: ResolverConfig::default(),
}
}
pub fn resolve_file_symbols(&mut self, filename: &str, source: &str) -> Result<Vec<ResolvedSymbol>, Box<dyn std::error::Error>> {
let mut resolved_symbols = Vec::new();
let imports: _ = self.parse_imports(source)?;
let exports: _ = self.parse_exports(source)?;
let usages: _ = self.parse_symbol_usages(source)?;
for usage in usages {
if let Some(symbol) = self.resolve_symbol(&usage.symbol_name, filename) {
resolved_symbols.push(ResolvedSymbol {
name: usage.symbol_name,
symbol_info: symbol,
usage_location: usage.location,
is_export: exports.contains(&usage.symbol_name),
});
}
}
self.import_graph.entry(filename.to_string()).or_insert_with(HashSet::new).extend(imports);
Ok(resolved_symbols)
}
fn parse_imports(&self, source: &str) -> Result<HashSet<String>, Box<dyn std::error::Error>> {
let mut imports = HashSet::new();
let lines: Vec<&str> = source.lines().collect();
for line in &lines {
let trimmed: _ = line.trim();
if trimmed.starts_with("import ") {
if let Some(imports_info) = self.parse_es6_import(trimmed) {
imports.extend(imports_info);
}
}
if trimmed.starts_with("const ") && trimmed.contains("require(") {
if let Some(require_info) = self.parse_require(trimmed) {
imports.insert(require_info);
}
}
}
Ok(imports)
}
fn parse_es6_import(&self, line: &str) -> Option<HashSet<String>> {
let mut imports = HashSet::new();
if line.contains("from ") {
if let Some(import_part) = line.split("from ").nth(1) {
let module_name: _ = import_part.trim().trim_matches('"').trim_matches('\'');
if line.contains('{') && line.contains('}') {
let start: _ = line.find('{')? + 1;
let end: _ = line.find('}')?;
let imported_items: _ = &line[start..end];
for item in imported_items.split(',') {
let symbol: _ = item.trim().split(" as ").next().unwrap_or(item.trim()).trim().to_string();
if !symbol.is_empty() {
imports.insert(symbol);
}
}
} else {
imports.insert("default".to_string());
}
}
}
if line.contains("* as ") {
if let Some(alias_start) = line.find("* as ") {
let alias_end: _ = line.find(" from ").unwrap_or(line.len());
let alias: _ = &line[alias_start + 5..alias_end];
imports.insert(alias.trim().to_string());
}
}
if imports.is_empty() {
None
} else {
Some(imports)
}
}
fn parse_require(&self, line: &str) -> Option<String> {
if let Some(require_start) = line.find("require(") {
let require_end: _ = line.find(')', require_start)?;
let module_path: _ = &line[require_start + 8..require_end];
let module_name: _ = module_path.trim().trim_matches('"').trim_matches('\'');
if module_name.starts_with('.') {
Some("local".to_string())
} else {
Some(module_name.to_string())
}
} else {
None
}
}
fn parse_exports(&self, source: &str) -> Result<HashSet<String>, Box<dyn std::error::Error>> {
let mut exports = HashSet::new();
let lines: Vec<&str> = source.lines().collect();
for line in &lines {
let trimmed: _ = line.trim();
if trimmed.starts_with("export {") {
if let Some(exports_part) = trimmed.strip_prefix("export {") {
let items: _ = exports_part.strip_suffix('}').unwrap_or(exports_part);
for item in items.split(',') {
exports.insert(item.trim().to_string());
}
}
}
if trimmed.starts_with("export default ") {
exports.insert("default".to_string());
}
if trimmed.starts_with("export function ") {
if let Some(name) = trimmed.split_whitespace().nth(2) {
exports.insert(name.to_string());
}
}
if trimmed.starts_with("export class ") {
if let Some(name) = trimmed.split_whitespace().nth(2) {
exports.insert(name.to_string());
}
}
if trimmed.starts_with("export const ") || trimmed.starts_with("export let ") || trimmed.starts_with("export var ") {
if let Some(name) = trimmed.split_whitespace().nth(2) {
exports.insert(name.to_string());
}
}
}
Ok(exports)
}
fn parse_symbol_usages(&self, source: &str) -> Result<Vec<SymbolUsage>, Box<dyn std::error::Error>> {
let mut usages = Vec::new();
let lines: Vec<&str> = source.lines().collect();
for (line_num, line) in lines.iter().enumerate() {
let trimmed: _ = line.trim();
if trimmed.starts_with("import ") || trimmed.starts_with("export ") {
continue;
}
if trimmed.starts_with("function ") || trimmed.starts_with("const ") || trimmed.starts_with("let ") || trimmed.starts_with("var ") {
continue;
}
if trimmed.starts_with("class ") || trimmed.starts_with("interface ") || trimmed.starts_with("type ") {
continue;
}
let words: Vec<&str> = trimmed.split_whitespace().collect();
for word in words {
let clean_word: _ = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != '$');
if !clean_word.is_empty() && self.is_likely_symbol(clean_word) {
usages.push(SymbolUsage {
symbol_name: clean_word.to_string(),
location: UsageLocation {
line: line_num + 1,
column: line.find(word).unwrap_or(0) + 1,
},
});
}
}
}
Ok(usages)
}
fn is_likely_symbol(&self, word: &str) -> bool {
let keywords: _ = ["if", "else", "for", "while", "return", "function", "class", "const", "let", "var", "this", "new", "try", "catch", "throw"];
let builtins: _ = ["console", "window", "document", "Math", "JSON", "Object", "Array", "String", "Number", "Boolean", "Date", "Promise"];
!keywords.contains(&word) && !builtins.contains(&word) && word.len() > 1
}
pub fn register_symbol(&mut self, symbol_name: String, symbol_info: SymbolInfo) {
self.symbol_table.insert(symbol_name, symbol_info);
}
pub fn resolve_symbol(&self, symbol_name: &str, current_file: &str) -> Option<SymbolInfo> {
if let Some(symbol) = self.symbol_table.get(symbol_name) {
return Some(symbol.clone());
}
if let Some(imports) = self.import_graph.get(current_file) {
for imported_module in imports {
let module_symbol_name: _ = format!("{}::{}, imported_module", symbol_name));
if let Some(symbol) = self.symbol_table.get(&module_symbol_name) {
return Some(symbol.clone());
}
}
}
None
}
pub fn get_import_graph(&self) -> &HashMap<String, HashSet<String> {
&self.import_graph
}
pub fn find_circular_dependencies(&self) -> Vec<Vec<String> {
let mut cycles = Vec::new();
let mut visited = HashSet::new();
let mut path = Vec::new();
for module in self.import_graph.keys() {
if !visited.contains(module) {
self.dfs_find_cycles(module, &mut visited, &mut path, &mut cycles);
}
}
cycles
}
fn dfs_find_cycles(
&self,
node: &str,
visited: &mut HashSet<String>,
path: &mut Vec<String>,
cycles: &mut Vec<Vec<String>>,
) {
visited.insert(node.to_string());
path.push(node.to_string());
if let Some(dependencies) = self.import_graph.get(node) {
for dep in dependencies {
if !visited.contains(dep) {
self.dfs_find_cycles(dep, visited, path, cycles);
} else if path.contains(dep) {
if let Some(pos) = path.iter().position(|p| p == dep) {
let cycle: _ = path[pos..].to_vec();
cycles.push(cycle);
}
}
}
}
path.pop();
}
pub fn get_unresolved_symbols(&self, usages: &[SymbolUsage]) -> Vec<String> {
let mut unresolved = Vec::new();
for usage in usages {
if !self.symbol_table.contains_key(&usage.symbol_name) {
unresolved.push(usage.symbol_name.clone());
}
}
unresolved
}
pub fn clear(&mut self) {
self.symbol_table.clear();
self.import_graph.clear();
}
}
#[derive(Debug, Clone)]
pub struct SymbolInfo {
pub name: String,
pub kind: SymbolKind,
pub file_path: String,
pub type_info: Option<Type>,
pub exported: bool,
pub js_doc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolKind {
Variable,
Function,
Class,
Interface,
TypeAlias,
Enum,
Namespace,
Module,
}
#[derive(Debug, Clone)]
pub struct SymbolUsage {
pub symbol_name: String,
pub location: UsageLocation,
}
#[derive(Debug, Clone)]
pub struct UsageLocation {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone)]
pub struct ResolvedSymbol {
pub name: String,
pub symbol_info: SymbolInfo,
pub usage_location: UsageLocation,
pub is_export: bool,
}
#[derive(Debug, Clone)]
pub struct ResolverConfig {
pub resolve_types: bool,
pub track_usages: bool,
pub check_circular_deps: bool,
}
impl Default for ResolverConfig {
fn default() -> Self {
Self {
resolve_types: true,
track_usages: true,
check_circular_deps: true,
}
}
}