normalize-languages 0.3.1

Tree-sitter language support and dynamic grammar loading
Documentation
//! HCL (HashiCorp Configuration Language) support.

use crate::{ContainerBody, Import, Language, LanguageSymbols};
use tree_sitter::Node;

/// HCL language support (Terraform, Packer, etc.).
pub struct Hcl;

impl Language for Hcl {
    fn name(&self) -> &'static str {
        "HCL"
    }
    fn extensions(&self) -> &'static [&'static str] {
        &["tf", "tfvars", "hcl"]
    }
    fn grammar_name(&self) -> &'static str {
        "hcl"
    }

    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
        Some(self)
    }

    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
        if node.kind() != "block" {
            return Vec::new();
        }

        let (block_type, _name) = match self.extract_block_info(node, content) {
            Some(info) => info,
            None => return Vec::new(),
        };

        if block_type != "module" {
            return Vec::new();
        }

        // Look for source attribute in the block
        let text = &content[node.byte_range()];
        for line in text.lines() {
            if line.trim().starts_with("source")
                && let Some(start) = line.find('"')
            {
                let rest = &line[start + 1..];
                if let Some(end) = rest.find('"') {
                    let module = rest[..end].to_string();
                    return vec![Import {
                        module,
                        names: Vec::new(),
                        alias: None,
                        is_wildcard: false,
                        is_relative: !rest.starts_with("registry") && !rest.starts_with("git"),
                        line: node.start_position().row + 1,
                    }];
                }
            }
        }

        Vec::new()
    }

    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
        format!("  source = \"{}\"", import.module)
    }

    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
        let name = symbol.name.as_str();
        match symbol.kind {
            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
            crate::SymbolKind::Module => name == "tests" || name == "test",
            _ => false,
        }
    }

    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
        node.child_by_field_name("body")
    }

    fn analyze_container_body(
        &self,
        body_node: &Node,
        content: &str,
        inner_indent: &str,
    ) -> Option<ContainerBody> {
        crate::body::analyze_brace_body(body_node, content, inner_indent)
    }

    fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
        None
    }
}

impl LanguageSymbols for Hcl {}

impl Hcl {
    fn extract_block_info(&self, node: &Node, content: &str) -> Option<(String, String)> {
        let mut cursor = node.walk();
        let mut block_type = None;
        let mut labels = Vec::new();

        for child in node.children(&mut cursor) {
            match child.kind() {
                "identifier" if block_type.is_none() => {
                    block_type = Some(content[child.byte_range()].to_string());
                }
                "string_lit" => {
                    let text = content[child.byte_range()].trim_matches('"').to_string();
                    labels.push(text);
                }
                _ => {}
            }
        }

        let block_type = block_type?;
        let name = if labels.len() >= 2 {
            format!("{}.{}", labels[0], labels[1])
        } else if !labels.is_empty() {
            labels[0].clone()
        } else {
            block_type.clone()
        };

        Some((block_type, name))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::validate_unused_kinds_audit;

    #[test]
    fn unused_node_kinds_audit() {
        // Run cross_check_node_kinds to populate this list
        #[rustfmt::skip]
        let documented_unused: &[&str] = &[
            "binary_operation", "body", "collection_value", "expression",
            "for_cond", "for_intro", "for_object_expr", "for_tuple_expr",
            "function_arguments", "function_call", "get_attr", "heredoc_identifier",
            "index", "literal_value", "object_elem", "quoted_template",
            "template_else_intro", "template_for", "template_for_end", "template_for_start",
            "template_if", "template_if_end", "template_if_intro", "tuple",
            "block_end", "block_start",
            // Comprehension — not a definition construct
            "for_expr",
        ];
        validate_unused_kinds_audit(&Hcl, documented_unused)
            .expect("HCL unused node kinds audit failed");
    }
}