normalize_languages/
hcl.rs1use crate::{ContainerBody, Import, Language, LanguageSymbols};
4use tree_sitter::Node;
5
6pub 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 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 #[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 "for_expr",
146 ];
147 validate_unused_kinds_audit(&Hcl, documented_unused)
148 .expect("HCL unused node kinds audit failed");
149 }
150}