Skip to main content

normalize_languages/
svelte.rs

1//! Svelte language support.
2
3use crate::component::extract_embedded_content;
4use crate::{ContainerBody, Import, Language, LanguageEmbedded, LanguageSymbols, Visibility};
5use tree_sitter::Node;
6
7/// Svelte language support.
8pub struct Svelte;
9
10impl Language for Svelte {
11    fn name(&self) -> &'static str {
12        "Svelte"
13    }
14    fn extensions(&self) -> &'static [&'static str] {
15        &["svelte"]
16    }
17    fn grammar_name(&self) -> &'static str {
18        "svelte"
19    }
20
21    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
22        Some(self)
23    }
24
25    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
26        if node.kind() != "import_statement" {
27            return Vec::new();
28        }
29
30        let text = &content[node.byte_range()];
31        let line = node.start_position().row + 1;
32
33        // Extract from clause
34        if let Some(from_idx) = text.find(" from ") {
35            let rest = &text[from_idx + 6..];
36            if let Some(start) = rest.find('"').or_else(|| rest.find('\'')) {
37                // start is a byte offset; slice at it (safe: ASCII quote is single-byte) then take first char
38                let Some(quote) = rest[start..].chars().next() else {
39                    return Vec::new();
40                };
41                let inner = &rest[start + 1..];
42                if let Some(end) = inner.find(quote) {
43                    let module = inner[..end].to_string();
44                    return vec![Import {
45                        module: module.clone(),
46                        names: Vec::new(),
47                        alias: None,
48                        is_wildcard: text.contains(" * "),
49                        is_relative: module.starts_with('.'),
50                        line,
51                    }];
52                }
53            }
54        }
55
56        Vec::new()
57    }
58
59    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
60        // Svelte uses JS import syntax
61        let names_to_use: Vec<&str> = names
62            .map(|n| n.to_vec())
63            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
64        if names_to_use.is_empty() {
65            format!("import '{}';", import.module)
66        } else {
67            format!(
68                "import {{ {} }} from '{}';",
69                names_to_use.join(", "),
70                import.module
71            )
72        }
73    }
74
75    fn get_visibility(&self, node: &Node, _content: &str) -> Visibility {
76        let mut cursor = node.walk();
77        for child in node.children(&mut cursor) {
78            if child.kind() == "export" {
79                return Visibility::Public;
80            }
81        }
82        Visibility::Private
83    }
84
85    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
86        let name = symbol.name.as_str();
87        match symbol.kind {
88            crate::SymbolKind::Function | crate::SymbolKind::Method => {
89                name.starts_with("test_")
90                    || name.starts_with("Test")
91                    || name == "describe"
92                    || name == "it"
93                    || name == "test"
94            }
95            crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
96            _ => false,
97        }
98    }
99
100    fn as_embedded(&self) -> Option<&dyn LanguageEmbedded> {
101        Some(self)
102    }
103
104    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
105        // Find the content of script/style elements
106        let mut cursor = node.walk();
107        node.children(&mut cursor)
108            .find(|&child| child.kind() == "raw_text")
109    }
110
111    fn analyze_container_body(
112        &self,
113        body_node: &Node,
114        content: &str,
115        inner_indent: &str,
116    ) -> Option<ContainerBody> {
117        // raw_text node from script/style element — content after leading newline
118        crate::body::analyze_end_body(body_node, content, inner_indent)
119    }
120
121    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
122        node.child_by_field_name("name")
123            .or_else(|| node.child_by_field_name("function"))
124            .map(|n| &content[n.byte_range()])
125    }
126}
127
128impl LanguageSymbols for Svelte {}
129
130impl LanguageEmbedded for Svelte {
131    fn embedded_content(&self, node: &Node, content: &str) -> Option<crate::EmbeddedBlock> {
132        extract_embedded_content(node, content)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::validate_unused_kinds_audit;
140
141    #[test]
142    fn unused_node_kinds_audit() {
143        // Run cross_check_node_kinds to populate
144        #[rustfmt::skip]
145        let documented_unused: &[&str] = &[
146            "await_end", "await_start", "block_end_tag", "block_start_tag",
147            "block_tag", "catch_block", "catch_start", "doctype", "else_block",
148            "else_if_start", "else_start", "expression", "expression_tag",
149            "if_end", "if_start", "key_statement", "snippet_statement", "then_block",
150            // Svelte template control flow — not symbol definitions
151            "await_statement", "each_statement", "else_if_block", "if_statement",
152        ];
153        validate_unused_kinds_audit(&Svelte, documented_unused)
154            .expect("Svelte unused node kinds audit failed");
155    }
156}