use crate::indexer::languages::Language;
use tree_sitter::Node;
pub struct JavaScript {}
impl Language for JavaScript {
fn name(&self) -> &'static str {
"javascript"
}
fn get_ts_language(&self) -> tree_sitter::Language {
tree_sitter_javascript::LANGUAGE.into()
}
fn get_meaningful_kinds(&self) -> Vec<&'static str> {
vec![
"function_declaration",
"method_definition",
"arrow_function",
"import_statement",
"export_statement",
]
}
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String> {
let mut symbols = Vec::new();
match node.kind() {
"function_declaration" | "method_definition" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "identifier" || child.kind().contains("name") {
if let Ok(n) = child.utf8_text(contents.as_bytes()) {
symbols.push(n.to_string());
}
break;
}
}
for child in node.children(&mut node.walk()) {
if child.kind() == "statement_block" {
self.extract_js_variable_declarations(child, contents, &mut symbols);
break;
}
}
}
"arrow_function" => {
if let Some(parent) = node.parent() {
if parent.kind() == "variable_declarator" {
for child in parent.children(&mut parent.walk()) {
if child.kind() == "identifier" {
if let Ok(n) = child.utf8_text(contents.as_bytes()) {
symbols.push(n.to_string());
}
break;
}
}
}
}
}
_ => self.extract_identifiers(node, contents, &mut symbols),
}
symbols.sort();
symbols.dedup();
symbols
}
fn extract_identifiers(&self, node: Node, contents: &str, symbols: &mut Vec<String>) {
let kind = node.kind();
if (kind.contains("identifier") || kind.contains("name")) && kind != "property_identifier" {
if let Ok(text) = node.utf8_text(contents.as_bytes()) {
let t = text.trim();
if !t.is_empty() && !symbols.contains(&t.to_string()) {
symbols.push(t.to_string());
}
}
}
if node.kind() == "member_expression" || node.kind() == "property_access_expression" {
let mut cursor = node.walk();
if cursor.goto_first_child() {
self.extract_identifiers(cursor.node(), contents, symbols);
return;
}
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
self.extract_identifiers(cursor.node(), contents, symbols);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn are_node_types_equivalent(&self, type1: &str, type2: &str) -> bool {
if type1 == type2 {
return true;
}
let semantic_groups = [
&[
"function_declaration",
"method_definition",
"arrow_function",
] as &[&str],
&["class_declaration", "method_definition"],
&["import_statement", "export_statement"],
&["variable_declaration", "lexical_declaration"],
];
for group in &semantic_groups {
let contains_type1 = group.contains(&type1);
let contains_type2 = group.contains(&type2);
if contains_type1 && contains_type2 {
return true;
}
}
false
}
fn get_node_type_description(&self, node_type: &str) -> &'static str {
match node_type {
"function_declaration" | "method_definition" | "arrow_function" => {
"function declarations"
}
"class_declaration" => "class declarations",
"import_statement" | "export_statement" => "import/export statements",
"variable_declaration" | "lexical_declaration" => "variable declarations",
_ => "declarations",
}
}
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
let mut imports = Vec::new();
let mut exports = Vec::new();
match node.kind() {
"import_statement" => {
if let Ok(import_text) = node.utf8_text(contents.as_bytes()) {
if let Some(imported_paths) = parse_js_import_statement_full_path(import_text) {
imports.extend(imported_paths);
}
}
}
"export_statement" => {
if let Ok(export_text) = node.utf8_text(contents.as_bytes()) {
if let Some(exported_items) = parse_js_export_statement(export_text) {
exports.extend(exported_items);
}
}
}
_ => {}
}
(imports, exports)
}
fn resolve_import(
&self,
import_path: &str,
source_file: &str,
all_files: &[String],
) -> Option<String> {
use super::resolution_utils::FileRegistry;
let registry = FileRegistry::new(all_files);
if import_path.starts_with("./") || import_path.starts_with("../") {
self.resolve_relative_import(import_path, source_file, ®istry)
} else if import_path.starts_with('/') {
self.resolve_absolute_import(import_path, ®istry)
} else {
self.resolve_module_import(import_path, source_file, ®istry)
}
}
fn get_file_extensions(&self) -> Vec<&'static str> {
vec!["js", "jsx", "mjs"]
}
}
impl JavaScript {
fn find_matching_js_file(
&self,
target_path: &std::path::Path,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
let target_str = target_path.to_string_lossy().to_string();
if let Some(exact_match) = registry.get_all_files().iter().find(|f| *f == &target_str) {
return Some(exact_match.clone());
}
if let Some(found) = crate::utils::path::PathNormalizer::find_path_in_collection(
&target_str,
registry.get_all_files(),
) {
return Some(found.to_string());
}
let extensions = ["js", "jsx", "mjs", "ts", "tsx"];
for ext in &extensions {
let with_ext = if target_str.ends_with(&format!(".{}", ext)) {
target_str.clone()
} else {
format!("{}.{}", target_str, ext)
};
if let Some(exact_match) = registry.get_all_files().iter().find(|f| *f == &with_ext) {
return Some(exact_match.clone());
}
let normalized_with_ext = with_ext.replace('\\', "/");
for js_file in registry.get_all_files() {
let normalized_js = js_file.replace('\\', "/");
if normalized_with_ext == normalized_js {
return Some(js_file.clone());
}
}
}
let index_candidates = [
"index.js",
"index.jsx",
"index.mjs",
"index.ts",
"index.tsx",
];
for index_file in &index_candidates {
let index_path = target_path.join(index_file);
let index_str = index_path.to_string_lossy().to_string();
if let Some(exact_match) = registry.get_all_files().iter().find(|f| *f == &index_str) {
return Some(exact_match.clone());
}
}
if let Ok(canonical_target) = target_path.canonicalize() {
let canonical_str = canonical_target.to_string_lossy().to_string();
for js_file in registry.get_all_files() {
if let Ok(canonical_js) = std::path::Path::new(js_file).canonicalize() {
let canonical_js_str = canonical_js.to_string_lossy().to_string();
if canonical_str == canonical_js_str {
return Some(js_file.clone());
}
}
}
}
if let Some(target_file_name) = target_path.file_name() {
if let Some(target_parent) = target_path.parent() {
for js_file in registry.get_all_files() {
let js_path = std::path::Path::new(js_file);
if let Some(js_file_name) = js_path.file_name() {
if let Some(js_parent) = js_path.parent() {
if target_file_name == js_file_name {
if let (Some(target_parent_str), Some(js_parent_str)) =
(target_parent.to_str(), js_parent.to_str())
{
if target_parent_str.ends_with(js_parent_str)
|| js_parent_str.ends_with(target_parent_str)
{
return Some(js_file.clone());
}
}
}
}
}
}
}
}
None
}
#[allow(clippy::only_used_in_recursion)]
pub fn extract_js_variable_declarations(
&self,
node: Node,
contents: &str,
symbols: &mut Vec<String>,
) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "variable_declaration" || child.kind() == "lexical_declaration" {
for var_decl in child.children(&mut child.walk()) {
if var_decl.kind() == "variable_declarator" {
for decl_child in var_decl.children(&mut var_decl.walk()) {
if decl_child.kind() == "identifier" {
if let Ok(name) = decl_child.utf8_text(contents.as_bytes()) {
let t = name.trim();
if !t.is_empty() && !symbols.contains(&t.to_string()) {
symbols.push(t.to_string());
}
}
break; }
}
}
}
}
else if child.kind() == "statement_block" || child.kind().contains("statement") {
self.extract_js_variable_declarations(child, contents, symbols);
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
}
pub fn parse_js_import_statement_full_path(import_text: &str) -> Option<Vec<String>> {
let mut imports = Vec::new();
let cleaned = import_text.trim();
if cleaned.contains(" from ") {
if let Some(from_pos) = cleaned.find(" from ") {
let module_part = &cleaned[from_pos + 6..]; let module_path = module_part
.trim()
.trim_start_matches('\'')
.trim_start_matches('"')
.trim_end_matches(';')
.trim_end_matches('\'')
.trim_end_matches('"');
if !module_path.is_empty() {
imports.push(module_path.to_string());
}
}
}
if !imports.is_empty() {
Some(imports)
} else {
None
}
}
pub fn parse_js_import_statement(import_text: &str) -> Option<Vec<String>> {
let mut imports = Vec::new();
let cleaned = import_text.trim();
if let Some(start) = cleaned.find('{') {
if let Some(end) = cleaned.find('}') {
let items = &cleaned[start + 1..end];
for item in items.split(',') {
let item = item.trim();
let name = if let Some(as_pos) = item.find(" as ") {
&item[..as_pos]
} else {
item
};
if !name.is_empty() {
imports.push(name.to_string());
}
}
return Some(imports);
}
}
if cleaned.starts_with("import ") && cleaned.contains(" from ") {
if let Some(from_pos) = cleaned.find(" from ") {
let import_part = &cleaned[7..from_pos].trim(); if !import_part.starts_with('{') && !import_part.starts_with('*') {
imports.push(import_part.to_string());
return Some(imports);
}
}
}
if cleaned.contains("* as ") {
if let Some(as_pos) = cleaned.find("* as ") {
if let Some(from_pos) = cleaned.find(" from ") {
let alias = &cleaned[as_pos + 5..from_pos].trim();
imports.push(alias.to_string());
return Some(imports);
}
}
}
None
}
pub fn parse_js_export_statement(export_text: &str) -> Option<Vec<String>> {
let mut exports = Vec::new();
let cleaned = export_text.trim();
let first_line = cleaned.lines().next().unwrap_or(cleaned);
if let Some(start) = first_line.find('{') {
if let Some(end) = first_line.find('}') {
let items = &first_line[start + 1..end];
for item in items.split(',') {
let item = item.trim();
let name = if let Some(as_pos) = item.find(" as ") {
&item[..as_pos]
} else {
item
};
if !name.is_empty() {
exports.push(name.to_string());
}
}
return Some(exports);
}
}
if let Some(rest) = first_line.strip_prefix("export ") {
if rest.starts_with("function ")
|| rest.starts_with("const ")
|| rest.starts_with("let ")
|| rest.starts_with("var ")
{
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[1].trim_end_matches('(').trim_end_matches('=');
exports.push(name.to_string());
return Some(exports);
}
}
if rest.starts_with("default class ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 3 {
let name = parts[2].trim_end_matches('{');
exports.push(name.to_string());
return Some(exports);
}
}
if rest.starts_with("class ") {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 2 {
let name = parts[1].trim_end_matches('{');
exports.push(name.to_string());
return Some(exports);
}
}
}
None
}
impl JavaScript {
fn resolve_relative_import(
&self,
import_path: &str,
source_file: &str,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
use super::resolution_utils::resolve_relative_path;
let relative_path = resolve_relative_path(source_file, import_path)?;
self.find_matching_js_file(&relative_path, registry)
}
fn resolve_absolute_import(
&self,
import_path: &str,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
let path = std::path::Path::new(import_path);
self.find_matching_js_file(path, registry)
}
fn resolve_module_import(
&self,
import_path: &str,
source_file: &str,
registry: &super::resolution_utils::FileRegistry,
) -> Option<String> {
let relative_import = format!("./{}", import_path);
if let Some(found) = self.resolve_relative_import(&relative_import, source_file, registry) {
return Some(found);
}
let source_dirs = ["src", "lib", "components", "utils", "modules"];
for src_dir in &source_dirs {
let src_path = std::path::Path::new(src_dir).join(import_path);
if let Some(found) = self.find_matching_js_file(&src_path, registry) {
return Some(found);
}
}
let node_modules_path = std::path::Path::new("node_modules").join(import_path);
if let Some(found) = self.find_matching_js_file(&node_modules_path, registry) {
return Some(found);
}
let package_patterns = [
format!("node_modules/{}/index.js", import_path),
format!("node_modules/{}/lib/index.js", import_path),
format!("node_modules/{}/src/index.js", import_path),
format!("node_modules/{}/dist/index.js", import_path),
];
for pattern in &package_patterns {
let package_path = std::path::Path::new(pattern);
if let Some(found) = self.find_matching_js_file(package_path, registry) {
return Some(found);
}
}
None
}
}