use crate::core::{FileId, Location, SymbolId, SymbolReference};
use oxc::ast::ast::*;
use oxc::ast_visit::walk;
use oxc::ast_visit::Visit;
use oxc::semantic::Semantic;
use oxc::span::Span;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct ReferenceExtractor<'a> {
file_path: PathBuf,
source: &'a str,
references: Vec<SymbolReference>,
imports: Vec<ImportInfo>,
pub has_dynamic_eval: bool,
}
#[derive(Debug, Clone)]
pub struct ImportInfo {
pub local_symbol_id: SymbolId,
pub imported_name: String,
pub resolved_path: PathBuf,
pub is_dynamic: bool,
pub location: Location,
}
impl<'a> ReferenceExtractor<'a> {
pub fn new(
file_path: PathBuf,
_file_id: FileId,
_symbol_map: &'a HashMap<oxc::semantic::SymbolId, SymbolId>,
source: &'a str,
) -> Self {
Self {
file_path,
source,
references: Vec::new(),
imports: Vec::new(),
has_dynamic_eval: false,
}
}
pub fn extract(
mut self,
program: &Program<'a>,
_semantic: &Semantic<'a>,
file_path: &Path,
) -> (Vec<SymbolReference>, Vec<ImportInfo>, bool) {
for stmt in &program.body {
if let Statement::ImportDeclaration(import) = stmt {
self.process_import(import, file_path);
}
}
self.visit_program(program);
(self.references, self.imports, self.has_dynamic_eval)
}
fn span_to_location(&self, span: Span) -> Location {
let (line, column) = self.offset_to_line_col(span.start);
Location::new(
self.file_path.clone(),
span.start,
span.end,
line,
column,
)
}
fn offset_to_line_col(&self, offset: u32) -> (u32, u32) {
let offset = offset as usize;
let mut line = 1u32;
let mut col = 1u32;
for (i, ch) in self.source.char_indices() {
if i >= offset {
break;
}
if ch == '\n' {
line += 1;
col = 1;
} else {
col += 1;
}
}
(line, col)
}
fn process_import(&mut self, import: &ImportDeclaration<'_>, file_path: &Path) {
let source_value = import.source.value.as_str();
let resolved_path = resolve_import_specifier(source_value, file_path);
if let Some(specifiers) = &import.specifiers {
for specifier in specifiers {
match specifier {
ImportDeclarationSpecifier::ImportSpecifier(spec) => {
let imported_name = spec.imported.name().to_string();
let local_name = spec.local.name.to_string();
let local_id = SymbolId::new(u32::MAX - self.imports.len() as u32);
self.imports.push(ImportInfo {
local_symbol_id: local_id,
imported_name,
resolved_path: resolved_path.clone(),
is_dynamic: false,
location: self.span_to_location(spec.span),
});
let _ = local_name;
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(spec) => {
let local_id = SymbolId::new(u32::MAX - self.imports.len() as u32);
self.imports.push(ImportInfo {
local_symbol_id: local_id,
imported_name: "default".to_string(),
resolved_path: resolved_path.clone(),
is_dynamic: false,
location: self.span_to_location(spec.span),
});
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => {
let local_id = SymbolId::new(u32::MAX - self.imports.len() as u32);
self.imports.push(ImportInfo {
local_symbol_id: local_id,
imported_name: "*".to_string(),
resolved_path: resolved_path.clone(),
is_dynamic: false,
location: self.span_to_location(spec.span),
});
}
}
}
}
}
}
impl<'a> Visit<'a> for ReferenceExtractor<'a> {
fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
if let Expression::Identifier(id) = &expr.callee {
if id.name == "eval" {
self.has_dynamic_eval = true;
}
}
if let Expression::Identifier(id) = &expr.callee {
if id.name == "Function" {
self.has_dynamic_eval = true;
}
}
if let Expression::StaticMemberExpression(member) = &expr.callee {
if let Expression::Identifier(obj) = &member.object {
if obj.name == "Reflect" {
self.has_dynamic_eval = true;
}
}
}
walk::walk_call_expression(self, expr);
}
fn visit_import_expression(&mut self, expr: &ImportExpression<'a>) {
if let Expression::StringLiteral(lit) = &expr.source {
let resolved = resolve_import_specifier(&lit.value, &self.file_path);
let local_id = SymbolId::new(u32::MAX - self.imports.len() as u32);
self.imports.push(ImportInfo {
local_symbol_id: local_id,
imported_name: "*".to_string(),
resolved_path: resolved,
is_dynamic: true,
location: self.span_to_location(expr.span),
});
} else {
self.has_dynamic_eval = true;
}
walk::walk_import_expression(self, expr);
}
fn visit_computed_member_expression(&mut self, expr: &ComputedMemberExpression<'a>) {
if !matches!(&expr.expression, Expression::StringLiteral(_) | Expression::NumericLiteral(_)) {
}
walk::walk_computed_member_expression(self, expr);
}
fn visit_jsx_element(&mut self, elem: &JSXElement<'a>) {
if let JSXElementName::Identifier(id) = &elem.opening_element.name {
if id.name.chars().next().map_or(false, |c| c.is_uppercase()) {
}
}
walk::walk_jsx_element(self, elem);
}
}
fn resolve_import_specifier(specifier: &str, from_file: &Path) -> PathBuf {
let from_dir = from_file.parent().unwrap_or(Path::new("."));
if specifier.starts_with('.') {
from_dir.join(specifier)
} else if specifier.starts_with('/') {
PathBuf::from(specifier)
} else {
PathBuf::from(format!("node_modules/{}", specifier))
}
}