Skip to main content

normalize_languages/
hcl.rs

1//! HCL (HashiCorp Configuration Language) support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols};
4use tree_sitter::Node;
5
6/// HCL language support (Terraform, Packer, etc.).
7pub struct Hcl;
8
9impl Language for Hcl {
10    fn name(&self) -> &'static str {
11        "HCL"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["tf", "tfvars", "hcl"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "hcl"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
25        if node.kind() != "block" {
26            return Vec::new();
27        }
28
29        let (block_type, _name) = match self.extract_block_info(node, content) {
30            Some(info) => info,
31            None => return Vec::new(),
32        };
33
34        if block_type != "module" {
35            return Vec::new();
36        }
37
38        // Look for source attribute in the block
39        let text = &content[node.byte_range()];
40        for line in text.lines() {
41            if line.trim().starts_with("source")
42                && let Some(start) = line.find('"')
43            {
44                let rest = &line[start + 1..];
45                if let Some(end) = rest.find('"') {
46                    let module = rest[..end].to_string();
47                    return vec![Import {
48                        module,
49                        names: Vec::new(),
50                        alias: None,
51                        is_wildcard: false,
52                        is_relative: !rest.starts_with("registry") && !rest.starts_with("git"),
53                        line: node.start_position().row + 1,
54                    }];
55                }
56            }
57        }
58
59        Vec::new()
60    }
61
62    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
63        format!("  source = \"{}\"", import.module)
64    }
65
66    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
67        let name = symbol.name.as_str();
68        match symbol.kind {
69            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
70            crate::SymbolKind::Module => name == "tests" || name == "test",
71            _ => false,
72        }
73    }
74
75    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
76        node.child_by_field_name("body")
77    }
78
79    fn analyze_container_body(
80        &self,
81        body_node: &Node,
82        content: &str,
83        inner_indent: &str,
84    ) -> Option<ContainerBody> {
85        crate::body::analyze_brace_body(body_node, content, inner_indent)
86    }
87
88    fn node_name<'a>(&self, _node: &Node, _content: &'a str) -> Option<&'a str> {
89        None
90    }
91}
92
93impl LanguageSymbols for Hcl {}
94
95impl Hcl {
96    fn extract_block_info(&self, node: &Node, content: &str) -> Option<(String, String)> {
97        let mut cursor = node.walk();
98        let mut block_type = None;
99        let mut labels = Vec::new();
100
101        for child in node.children(&mut cursor) {
102            match child.kind() {
103                "identifier" if block_type.is_none() => {
104                    block_type = Some(content[child.byte_range()].to_string());
105                }
106                "string_lit" => {
107                    let text = content[child.byte_range()].trim_matches('"').to_string();
108                    labels.push(text);
109                }
110                _ => {}
111            }
112        }
113
114        let block_type = block_type?;
115        let name = if labels.len() >= 2 {
116            format!("{}.{}", labels[0], labels[1])
117        } else if !labels.is_empty() {
118            labels[0].clone()
119        } else {
120            block_type.clone()
121        };
122
123        Some((block_type, name))
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::validate_unused_kinds_audit;
131
132    #[test]
133    fn unused_node_kinds_audit() {
134        // Run cross_check_node_kinds to populate this list
135        #[rustfmt::skip]
136        let documented_unused: &[&str] = &[
137            "binary_operation", "body", "collection_value", "expression",
138            "for_cond", "for_intro", "for_object_expr", "for_tuple_expr",
139            "function_arguments", "function_call", "get_attr", "heredoc_identifier",
140            "index", "literal_value", "object_elem", "quoted_template",
141            "template_else_intro", "template_for", "template_for_end", "template_for_start",
142            "template_if", "template_if_end", "template_if_intro", "tuple",
143            "block_end", "block_start",
144            // Comprehension — not a definition construct
145            "for_expr",
146        ];
147        validate_unused_kinds_audit(&Hcl, documented_unused)
148            .expect("HCL unused node kinds audit failed");
149    }
150}