use crate::{ContainerBody, Import, Language, LanguageSymbols};
use tree_sitter::Node;
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();
}
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() {
#[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",
"for_expr",
];
validate_unused_kinds_audit(&Hcl, documented_unused)
.expect("HCL unused node kinds audit failed");
}
}