1use crate::component::extract_embedded_content;
4use crate::external_packages::ResolvedPackage;
5use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
6use std::path::{Path, PathBuf};
7use tree_sitter::Node;
8
9pub struct Svelte;
11
12impl Language for Svelte {
13 fn name(&self) -> &'static str {
14 "Svelte"
15 }
16 fn extensions(&self) -> &'static [&'static str] {
17 &["svelte"]
18 }
19 fn grammar_name(&self) -> &'static str {
20 "svelte"
21 }
22
23 fn has_symbols(&self) -> bool {
24 true
25 }
26
27 fn container_kinds(&self) -> &'static [&'static str] {
28 &["script_element", "style_element"]
29 }
30
31 fn function_kinds(&self) -> &'static [&'static str] {
32 &[] }
34
35 fn type_kinds(&self) -> &'static [&'static str] {
36 &[]
37 }
38
39 fn import_kinds(&self) -> &'static [&'static str] {
40 &[] }
42
43 fn public_symbol_kinds(&self) -> &'static [&'static str] {
44 &[] }
46
47 fn visibility_mechanism(&self) -> VisibilityMechanism {
48 VisibilityMechanism::ExplicitExport
49 }
50
51 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
52 let text = &content[node.byte_range()];
54
55 if node.kind() == "export_statement" || text.contains("export ") {
56 if let Some(name) = self.node_name(node, content) {
57 let kind = if text.contains("function") {
58 SymbolKind::Function
59 } else {
60 SymbolKind::Variable
61 };
62
63 return vec![Export {
64 name: name.to_string(),
65 kind,
66 line: node.start_position().row + 1,
67 }];
68 }
69 }
70
71 Vec::new()
72 }
73
74 fn scope_creating_kinds(&self) -> &'static [&'static str] {
75 &["if_statement", "each_statement", "await_statement"]
76 }
77
78 fn control_flow_kinds(&self) -> &'static [&'static str] {
79 &["if_statement", "each_statement", "await_statement"]
80 }
81
82 fn complexity_nodes(&self) -> &'static [&'static str] {
83 &["if_statement", "each_statement", "else_if_block"]
84 }
85
86 fn nesting_nodes(&self) -> &'static [&'static str] {
87 &[
88 "if_statement",
89 "each_statement",
90 "await_statement",
91 "script_element",
92 ]
93 }
94
95 fn signature_suffix(&self) -> &'static str {
96 ""
97 }
98
99 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
100 let name = self.node_name(node, content)?;
101 let text = &content[node.byte_range()];
102 let first_line = text.lines().next().unwrap_or(text);
103
104 Some(Symbol {
105 name: name.to_string(),
106 kind: SymbolKind::Function,
107 signature: first_line.trim().to_string(),
108 docstring: self.extract_docstring(node, content),
109 attributes: Vec::new(),
110 start_line: node.start_position().row + 1,
111 end_line: node.end_position().row + 1,
112 visibility: self.get_visibility(node, content),
113 children: Vec::new(),
114 is_interface_impl: false,
115 implements: Vec::new(),
116 })
117 }
118
119 fn extract_container(&self, node: &Node, _content: &str) -> Option<Symbol> {
120 let kind = match node.kind() {
121 "script_element" => SymbolKind::Module,
122 "style_element" => SymbolKind::Class,
123 _ => return None,
124 };
125
126 let name = if node.kind() == "script_element" {
127 "<script>".to_string()
128 } else {
129 "<style>".to_string()
130 };
131
132 Some(Symbol {
133 name: name.clone(),
134 kind,
135 signature: name,
136 docstring: None,
137 attributes: Vec::new(),
138 start_line: node.start_position().row + 1,
139 end_line: node.end_position().row + 1,
140 visibility: Visibility::Public,
141 children: Vec::new(),
142 is_interface_impl: false,
143 implements: Vec::new(),
144 })
145 }
146
147 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
148 None
149 }
150
151 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
152 let mut prev = node.prev_sibling();
154 let mut doc_lines = Vec::new();
155
156 while let Some(sibling) = prev {
157 let text = &content[sibling.byte_range()];
158 if sibling.kind() == "comment" {
159 if text.starts_with("/**") {
160 let inner = text
161 .strip_prefix("/**")
162 .unwrap_or(text)
163 .strip_suffix("*/")
164 .unwrap_or(text);
165 let lines: Vec<&str> = inner
166 .lines()
167 .map(|l| l.trim().trim_start_matches('*').trim())
168 .filter(|l| !l.is_empty() && !l.starts_with('@'))
169 .collect();
170 if !lines.is_empty() {
171 return Some(lines.join(" "));
172 }
173 } else if text.starts_with("//") {
174 doc_lines.push(text.strip_prefix("//").unwrap_or(text).trim().to_string());
175 }
176 prev = sibling.prev_sibling();
177 } else {
178 break;
179 }
180 }
181
182 if doc_lines.is_empty() {
183 return None;
184 }
185
186 doc_lines.reverse();
187 Some(doc_lines.join(" "))
188 }
189
190 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
191 Vec::new()
192 }
193
194 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
195 if node.kind() != "import_statement" {
196 return Vec::new();
197 }
198
199 let text = &content[node.byte_range()];
200 let line = node.start_position().row + 1;
201
202 if let Some(from_idx) = text.find(" from ") {
204 let rest = &text[from_idx + 6..];
205 if let Some(start) = rest.find('"').or_else(|| rest.find('\'')) {
206 let quote = rest.chars().nth(start).unwrap();
207 let inner = &rest[start + 1..];
208 if let Some(end) = inner.find(quote) {
209 let module = inner[..end].to_string();
210 return vec![Import {
211 module: module.clone(),
212 names: Vec::new(),
213 alias: None,
214 is_wildcard: text.contains(" * "),
215 is_relative: module.starts_with('.'),
216 line,
217 }];
218 }
219 }
220 }
221
222 Vec::new()
223 }
224
225 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
226 let names_to_use: Vec<&str> = names
228 .map(|n| n.to_vec())
229 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
230 if names_to_use.is_empty() {
231 format!("import '{}';", import.module)
232 } else {
233 format!(
234 "import {{ {} }} from '{}';",
235 names_to_use.join(", "),
236 import.module
237 )
238 }
239 }
240
241 fn is_public(&self, node: &Node, content: &str) -> bool {
242 let text = &content[node.byte_range()];
243 text.contains("export ")
244 }
245
246 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
247 if self.is_public(node, content) {
248 Visibility::Public
249 } else {
250 Visibility::Private
251 }
252 }
253
254 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
255 let name = symbol.name.as_str();
256 match symbol.kind {
257 crate::SymbolKind::Function | crate::SymbolKind::Method => {
258 name.starts_with("test_")
259 || name.starts_with("Test")
260 || name == "describe"
261 || name == "it"
262 || name == "test"
263 }
264 crate::SymbolKind::Module => name == "tests" || name == "test" || name == "__tests__",
265 _ => false,
266 }
267 }
268
269 fn embedded_content(&self, node: &Node, content: &str) -> Option<crate::EmbeddedBlock> {
270 extract_embedded_content(node, content)
271 }
272
273 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
274 let mut cursor = node.walk();
276 for child in node.children(&mut cursor) {
277 if child.kind() == "raw_text" {
278 return Some(child);
279 }
280 }
281 None
282 }
283
284 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
285 false
286 }
287
288 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
289 node.child_by_field_name("name")
290 .or_else(|| node.child_by_field_name("function"))
291 .map(|n| &content[n.byte_range()])
292 }
293
294 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
295 let ext = path.extension()?.to_str()?;
296 if ext != "svelte" {
297 return None;
298 }
299 let stem = path.file_stem()?.to_str()?;
300 Some(stem.to_string())
301 }
302
303 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
304 vec![
305 format!("{}.svelte", module),
306 format!("src/lib/{}.svelte", module),
307 format!("src/routes/{}.svelte", module),
308 ]
309 }
310
311 fn lang_key(&self) -> &'static str {
312 "svelte"
313 }
314
315 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
316 import_name == "svelte"
317 || import_name.starts_with("svelte/")
318 || import_name.starts_with("$app/")
319 || import_name.starts_with("$lib/")
320 }
321
322 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
323 None
324 }
325
326 fn resolve_local_import(
327 &self,
328 import: &str,
329 current_file: &Path,
330 project_root: &Path,
331 ) -> Option<PathBuf> {
332 if import.starts_with('.') {
334 if let Some(dir) = current_file.parent() {
335 let candidates = [
336 import.to_string(),
337 format!("{}.svelte", import),
338 format!("{}/index.svelte", import),
339 ];
340 for c in &candidates {
341 let full = dir.join(c);
342 if full.is_file() {
343 return Some(full);
344 }
345 }
346 }
347 }
348
349 if import.starts_with("$lib/") {
351 let rest = import.strip_prefix("$lib/")?;
352 let lib_dir = project_root.join("src/lib");
353 let candidates = [
354 rest.to_string(),
355 format!("{}.svelte", rest),
356 format!("{}.js", rest),
357 format!("{}.ts", rest),
358 ];
359 for c in &candidates {
360 let full = lib_dir.join(c);
361 if full.is_file() {
362 return Some(full);
363 }
364 }
365 }
366
367 None
368 }
369
370 fn resolve_external_import(
371 &self,
372 _import_name: &str,
373 _project_root: &Path,
374 ) -> Option<ResolvedPackage> {
375 None
377 }
378
379 fn get_version(&self, project_root: &Path) -> Option<String> {
380 let pkg_json = project_root.join("package.json");
381 if pkg_json.is_file() {
382 if let Ok(content) = std::fs::read_to_string(&pkg_json) {
383 if let Some(idx) = content.find("\"svelte\"") {
385 let rest = &content[idx..];
386 if let Some(colon) = rest.find(':') {
387 let after = rest[colon + 1..].trim();
388 if let Some(start) = after.find('"') {
389 let inner = &after[start + 1..];
390 if let Some(end) = inner.find('"') {
391 return Some(inner[..end].to_string());
392 }
393 }
394 }
395 }
396 }
397 }
398 None
399 }
400
401 fn find_package_cache(&self, project_root: &Path) -> Option<PathBuf> {
402 let node_modules = project_root.join("node_modules");
403 if node_modules.is_dir() {
404 return Some(node_modules);
405 }
406 None
407 }
408
409 fn indexable_extensions(&self) -> &'static [&'static str] {
410 &["svelte"]
411 }
412 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
413 Vec::new()
414 }
415
416 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
417 use crate::traits::{has_extension, skip_dotfiles};
418 if skip_dotfiles(name) {
419 return true;
420 }
421 if is_dir && (name == "node_modules" || name == ".svelte-kit" || name == "build") {
422 return true;
423 }
424 !is_dir && !has_extension(name, self.indexable_extensions())
425 }
426
427 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
428 Vec::new()
429 }
430
431 fn package_module_name(&self, entry_name: &str) -> String {
432 entry_name
433 .strip_suffix(".svelte")
434 .unwrap_or(entry_name)
435 .to_string()
436 }
437
438 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
439 if path.is_file() {
440 Some(path.to_path_buf())
441 } else {
442 None
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::validate_unused_kinds_audit;
451
452 #[test]
453 fn unused_node_kinds_audit() {
454 #[rustfmt::skip]
456 let documented_unused: &[&str] = &[
457 "await_end", "await_start", "block_end_tag", "block_start_tag",
458 "block_tag", "catch_block", "catch_start", "doctype", "else_block",
459 "else_if_start", "else_start", "expression", "expression_tag",
460 "if_end", "if_start", "key_statement", "snippet_statement", "then_block",
461 ];
462 validate_unused_kinds_audit(&Svelte, documented_unused)
463 .expect("Svelte unused node kinds audit failed");
464 }
465}