1use crate::{ContainerBody, Import, Language, LanguageSymbols, SymbolKind, Visibility};
4use tree_sitter::Node;
5
6pub struct Scss;
8
9impl Language for Scss {
10 fn name(&self) -> &'static str {
11 "SCSS"
12 }
13 fn extensions(&self) -> &'static [&'static str] {
14 &["scss", "sass"]
15 }
16 fn grammar_name(&self) -> &'static str {
17 "scss"
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 let text = &content[node.byte_range()];
26 let line = node.start_position().row + 1;
27
28 for keyword in &["@import ", "@use ", "@forward "] {
30 if let Some(stripped) = text.strip_prefix(keyword) {
31 let rest = stripped.trim();
32 if let Some(start) = rest.find('"').or_else(|| rest.find('\'')) {
34 let Some(quote) = rest[start..].chars().next() else {
36 continue;
37 };
38 let inner = &rest[start + 1..];
39 if let Some(end) = inner.find(quote) {
40 let module = inner[..end].to_string();
41 return vec![Import {
42 module,
43 names: Vec::new(),
44 alias: None,
45 is_wildcard: false,
46 is_relative: true,
47 line,
48 }];
49 }
50 }
51 }
52 }
53
54 Vec::new()
55 }
56
57 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
58 format!("@import \"{}\"", import.module)
60 }
61
62 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
63 if let Some(name) = self.node_name(node, content) {
64 if name.starts_with('_') {
65 Visibility::Private
66 } else {
67 Visibility::Public
68 }
69 } else {
70 Visibility::Public
71 }
72 }
73
74 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
75 let name = symbol.name.as_str();
76 match symbol.kind {
77 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
78 crate::SymbolKind::Module => name == "tests" || name == "test",
79 _ => false,
80 }
81 }
82
83 fn refine_kind(&self, node: &Node, _content: &str, tag_kind: SymbolKind) -> SymbolKind {
84 match node.kind() {
85 "media_statement" | "supports_statement" | "keyframes_statement" => SymbolKind::Module,
86 _ => tag_kind,
87 }
88 }
89
90 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
91 match node.kind() {
92 "mixin_statement" | "function_statement" => {
93 let name_node = node.child_by_field_name("name")?;
94 Some(content[name_node.byte_range()].trim())
95 }
96 "rule_set" => {
97 let mut cursor = node.walk();
98 for child in node.children(&mut cursor) {
99 if child.kind() == "selectors" {
100 return Some(content[child.byte_range()].trim());
101 }
102 }
103 None
104 }
105 "media_statement" => extract_at_rule_name(node, content, "@media"),
106 "supports_statement" => extract_at_rule_name(node, content, "@supports"),
107 "keyframes_statement" => {
108 let mut cursor = node.walk();
109 for child in node.children(&mut cursor) {
110 if child.kind() == "keyframes_name" {
111 return Some(content[child.byte_range()].trim());
112 }
113 }
114 None
115 }
116 "declaration" => {
117 let mut cursor = node.walk();
118 for child in node.children(&mut cursor) {
119 if child.kind() == "property_name" {
120 return Some(content[child.byte_range()].trim());
121 }
122 }
123 None
124 }
125 _ => node
126 .child_by_field_name("name")
127 .map(|n| content[n.byte_range()].trim()),
128 }
129 }
130
131 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
132 match node.kind() {
133 "rule_set" | "media_statement" | "supports_statement" | "keyframes_statement" => {
134 let mut cursor = node.walk();
135 for child in node.children(&mut cursor) {
136 if child.kind() == "block" || child.kind() == "keyframe_block_list" {
137 return Some(child);
138 }
139 }
140 None
141 }
142 _ => node
143 .child_by_field_name("body")
144 .or_else(|| node.child_by_field_name("block")),
145 }
146 }
147
148 fn build_signature(&self, node: &Node, content: &str) -> String {
149 if let Some(name) = self.node_name(node, content) {
150 match node.kind() {
151 "mixin_statement" => {
152 let mut cursor = node.walk();
154 for child in node.children(&mut cursor) {
155 if child.kind() == "parameters" {
156 let params = content[child.byte_range()].trim();
157 return format!(
158 "@mixin {}({}) {{ … }}",
159 name,
160 params.trim_matches(|c| c == '(' || c == ')')
161 );
162 }
163 }
164 format!("@mixin {} {{ … }}", name)
165 }
166 "function_statement" => {
167 let mut cursor = node.walk();
168 for child in node.children(&mut cursor) {
169 if child.kind() == "parameters" {
170 let params = content[child.byte_range()].trim();
171 return format!(
172 "@function {}({}) {{ … }}",
173 name,
174 params.trim_matches(|c| c == '(' || c == ')')
175 );
176 }
177 }
178 format!("@function {} {{ … }}", name)
179 }
180 "rule_set" => format!("{} {{ … }}", name),
181 "media_statement" => format!("@media {} {{ … }}", name),
182 "supports_statement" => format!("@supports {} {{ … }}", name),
183 "keyframes_statement" => format!("@keyframes {} {{ … }}", name),
184 "declaration" => {
185 let mut cursor = node.walk();
186 let mut found_name = false;
187 for child in node.children(&mut cursor) {
188 if child.kind() == "property_name" {
189 found_name = true;
190 } else if found_name && child.kind() != ":" && child.kind() != ";" {
191 let val = content[child.byte_range()].trim();
192 if val.len() > 40 {
193 return format!("{}: {}…", name, &val[..37]);
194 }
195 return format!("{}: {}", name, val);
196 }
197 }
198 name.to_string()
199 }
200 _ => name.to_string(),
201 }
202 } else {
203 content[node.byte_range()]
204 .lines()
205 .next()
206 .unwrap_or("")
207 .trim()
208 .to_string()
209 }
210 }
211
212 fn analyze_container_body(
213 &self,
214 body_node: &Node,
215 content: &str,
216 inner_indent: &str,
217 ) -> Option<ContainerBody> {
218 crate::body::analyze_brace_body(body_node, content, inner_indent)
219 }
220}
221
222impl LanguageSymbols for Scss {}
223
224fn extract_at_rule_name<'a>(node: &Node, content: &'a str, keyword: &str) -> Option<&'a str> {
226 let full = &content[node.byte_range()];
227 let after_keyword = full.strip_prefix(keyword)?.trim_start();
228 let name = after_keyword.split('{').next()?.trim();
229 if name.is_empty() {
230 return None;
231 }
232 let start = node.start_byte() + full.find(name)?;
233 let end = start + name.len();
234 Some(&content[start..end])
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::validate_unused_kinds_audit;
241
242 #[test]
243 fn unused_node_kinds_audit() {
244 #[rustfmt::skip]
246 let documented_unused: &[&str] = &[
247 "at_root_statement", "binary_expression", "call_expression",
248 "charset_statement", "class_name", "class_selector", "debug_statement",
249 "else_clause", "else_if_clause", "error_statement",
250 "extend_statement", "function_name", "identifier", "important",
251 "important_value", "include_statement", "keyframe_block",
252 "keyframe_block_list",
253 "namespace_statement", "postcss_statement", "pseudo_class_selector",
254 "return_statement", "scope_statement", "warn_statement",
255 "block",
257 "each_statement", "for_statement", "if_statement", "while_statement",
259 "forward_statement", "import_statement", "use_statement",
261
262 ];
263 validate_unused_kinds_audit(&Scss, documented_unused)
264 .expect("SCSS unused node kinds audit failed");
265 }
266}