use crate::indexer::languages::Language;
use tree_sitter::Node;
pub struct Bash {}
impl Language for Bash {
fn name(&self) -> &'static str {
"bash"
}
fn get_ts_language(&self) -> tree_sitter::Language {
tree_sitter_bash::LANGUAGE.into()
}
fn get_meaningful_kinds(&self) -> Vec<&'static str> {
vec!["function_definition", "command"] }
fn extract_symbols(&self, node: Node, contents: &str) -> Vec<String> {
let mut symbols = Vec::new();
match node.kind() {
"function_definition" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "name" {
if let Ok(name) = child.utf8_text(contents.as_bytes()) {
symbols.push(name.to_string());
}
break;
}
}
self.extract_bash_variables(node, contents, &mut symbols);
}
_ => 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 == "variable_name" || kind == "command_name" {
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());
}
}
}
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_definition"] as &[&str],
&["variable_assignment"],
&["command", "simple_command"],
];
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_definition" => "function declarations",
"variable_assignment" => "variable assignments",
"command" | "simple_command" => "command declarations",
_ => "declarations",
}
}
fn extract_imports_exports(&self, node: Node, contents: &str) -> (Vec<String>, Vec<String>) {
let mut imports = Vec::new();
let exports = Vec::new();
if node.kind() == "function_definition" {
Self::extract_bash_sources_from_node(node, contents, &mut imports);
}
(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_source(import_path, source_file, ®istry)
} else {
registry.find_exact_file(import_path)
}
}
fn get_file_extensions(&self) -> Vec<&'static str> {
vec!["sh", "bash"]
}
}
impl Bash {
fn extract_bash_variables(&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_assignment" {
for var_child in child.children(&mut child.walk()) {
if var_child.kind() == "variable_name" {
if let Ok(var_name) = var_child.utf8_text(contents.as_bytes()) {
symbols.push(var_name.to_string());
}
break;
}
}
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn extract_bash_sources_from_node(node: Node, contents: &str, imports: &mut Vec<String>) {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "command" {
if let Ok(command_text) = child.utf8_text(contents.as_bytes()) {
if let Some(source_file) = Self::parse_bash_source(command_text) {
imports.push(source_file);
}
}
}
Self::extract_bash_sources_from_node(child, contents, imports);
if !cursor.goto_next_sibling() {
break;
}
}
}
}
fn parse_bash_source(command_text: &str) -> Option<String> {
let trimmed = command_text.trim();
if let Some(stripped) = trimmed.strip_prefix("source ") {
if let Some(filename) = stripped.split_whitespace().next() {
return Some(filename.trim_matches('"').trim_matches('\'').to_string());
}
} else if let Some(stripped) = trimmed.strip_prefix(". ") {
if let Some(filename) = stripped.split_whitespace().next() {
return Some(filename.trim_matches('"').trim_matches('\'').to_string());
}
}
None
}
fn resolve_relative_source(
&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)?;
registry.find_exact_file(&relative_path.to_string_lossy())
}
}