diff --git a/Cargo.lock b/Cargo.lock index d1cbc7ba..6c8c3a1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,7 @@ dependencies = [ "tree-sitter-php", "tree-sitter-python", "tree-sitter-rust", + "tree-sitter-svelte-next", "tree-sitter-swift", "tree-sitter-typescript", "walkdir", @@ -5413,6 +5414,16 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "tree-sitter-svelte-next" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f88190d0743e897c3e148a7e241aba0a8844b8afe816943851426e3f7b9753" +dependencies = [ + "cc", + "tree-sitter-language", +] + [[package]] name = "tree-sitter-swift" version = "0.7.2" diff --git a/Cargo.toml b/Cargo.toml index 62cb5cf9..f506f153 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,6 +105,7 @@ async-trait = "0.1.89" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } sysinfo = "0.39.2" indexmap = { version = "2.14.0", features = ["serde"] } +tree-sitter-svelte-next = "0.1.1" [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } diff --git a/contributing/parsers/svelte/AUDIT_REPORT.md b/contributing/parsers/svelte/AUDIT_REPORT.md new file mode 100644 index 00000000..6c681c03 --- /dev/null +++ b/contributing/parsers/svelte/AUDIT_REPORT.md @@ -0,0 +1,66 @@ +# Svelte Parser Symbol Extraction Coverage Report + +*Generated: 2026-05-22 19:33:09 UTC* + +## Summary +- Key nodes: 5/20 (25%) +- Symbol kinds extracted: 4 + +> **Note:** Svelte delegates ` + + + +
+

{title} (v{COMPONENT_VERSION})

+ + + {#snippet userRow(user: User)} +
  • {greeting(user)}
  • + {/snippet} + + + +

    Doubled: {doubled}

    + + {#if isMaxed} + Maxed out! + {:else} + + + {/if} + + + + +
    diff --git a/flake.lock b/flake.lock index fe873248..5feb4391 100644 --- a/flake.lock +++ b/flake.lock @@ -109,11 +109,11 @@ ] }, "locked": { - "lastModified": 1774062094, - "narHash": "sha256-ba3c+hS7KzEiwtZRGHagIAYdcmdY3rCSWVCyn64rx7s=", + "lastModified": 1779333539, + "narHash": "sha256-lpmN2lrBDZDPjov2cbD3bOOJsI0fkKolKXasYPCqSys=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "c807e83cc2e32adc35f51138b3bdef722c0812ab", + "rev": "672fa5fc5608d5cd82286a6f69aaf84a40b4fe41", "type": "github" }, "original": { diff --git a/src/io/parse.rs b/src/io/parse.rs index b059d337..3385d5aa 100644 --- a/src/io/parse.rs +++ b/src/io/parse.rs @@ -265,6 +265,7 @@ pub fn execute_parse( Language::Kotlin => tree_sitter_kotlin::language(), Language::Lua => tree_sitter_lua::LANGUAGE.into(), Language::Swift => tree_sitter_swift::LANGUAGE.into(), + Language::Svelte => tree_sitter_svelte_next::LANGUAGE.into(), }; parser diff --git a/src/parsing/factory.rs b/src/parsing/factory.rs index d35037a3..1ed75a02 100644 --- a/src/parsing/factory.rs +++ b/src/parsing/factory.rs @@ -8,8 +8,8 @@ use super::{ CppParser, GdscriptBehavior, GdscriptParser, GoBehavior, GoParser, JavaBehavior, JavaParser, JavaScriptBehavior, JavaScriptParser, KotlinBehavior, KotlinParser, Language, LanguageBehavior, LanguageId, LanguageParser, LuaBehavior, LuaParser, PhpBehavior, PhpParser, PythonBehavior, - PythonParser, RustBehavior, RustParser, SwiftBehavior, SwiftParser, TypeScriptBehavior, - TypeScriptParser, get_registry, + PythonParser, RustBehavior, RustParser, SvelteBehavior, SvelteParser, SwiftBehavior, + SwiftParser, TypeScriptBehavior, TypeScriptParser, get_registry, }; use crate::{IndexError, IndexResult, Settings}; use std::sync::Arc; @@ -190,6 +190,10 @@ impl ParserFactory { let parser = SwiftParser::new().map_err(|e| IndexError::General(e.to_string()))?; Ok(Box::new(parser)) } + Language::Svelte => { + let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?; + Ok(Box::new(parser)) + } } } @@ -336,6 +340,13 @@ impl ParserFactory { behavior: Box::new(SwiftBehavior::new()), } } + Language::Svelte => { + let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?; + ParserWithBehavior { + parser: Box::new(parser), + behavior: Box::new(SvelteBehavior::new()), + } + } }; Ok(result) @@ -376,6 +387,7 @@ impl ParserFactory { Language::Php, Language::Python, Language::Rust, + Language::Svelte, Language::Swift, Language::TypeScript, ] diff --git a/src/parsing/language.rs b/src/parsing/language.rs index e0d85092..35e5916e 100644 --- a/src/parsing/language.rs +++ b/src/parsing/language.rs @@ -23,6 +23,7 @@ pub enum Language { Kotlin, Lua, Swift, + Svelte, } impl Language { @@ -48,6 +49,7 @@ impl Language { Language::Kotlin => super::LanguageId::new("kotlin"), Language::Lua => super::LanguageId::new("lua"), Language::Swift => super::LanguageId::new("swift"), + Language::Svelte => super::LanguageId::new("svelte"), } } @@ -72,6 +74,7 @@ impl Language { "kotlin" => Some(Language::Kotlin), "lua" => Some(Language::Lua), "swift" => Some(Language::Swift), + "svelte" => Some(Language::Svelte), _ => None, } } @@ -111,6 +114,7 @@ impl Language { "kt" | "kts" => Some(Language::Kotlin), "lua" => Some(Language::Lua), "swift" => Some(Language::Swift), + "svelte" => Some(Language::Svelte), _ => None, } } @@ -142,6 +146,7 @@ impl Language { Language::Kotlin => &["kt", "kts"], Language::Lua => &["lua"], Language::Swift => &["swift"], + Language::Svelte => &["svelte"], } } @@ -163,6 +168,7 @@ impl Language { Language::Kotlin => "kotlin", Language::Lua => "lua", Language::Swift => "swift", + Language::Svelte => "svelte", } } @@ -184,6 +190,7 @@ impl Language { Language::Kotlin => "Kotlin", Language::Lua => "Lua", Language::Swift => "Swift", + Language::Svelte => "Svelte", } } } diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs index 2837f68c..4ba3951e 100644 --- a/src/parsing/mod.rs +++ b/src/parsing/mod.rs @@ -22,6 +22,7 @@ pub mod python; pub mod registry; pub mod resolution; pub mod rust; +pub mod svelte; pub mod swift; pub mod typescript; @@ -58,5 +59,6 @@ pub use resolution::{ PipelineSymbolCache, ResolutionScope, ResolveResult, ScopeLevel, }; pub use rust::{RustBehavior, RustParser}; +pub use svelte::{SvelteBehavior, SvelteParser}; pub use swift::{SwiftBehavior, SwiftParser}; pub use typescript::{TypeScriptBehavior, TypeScriptParser}; diff --git a/src/parsing/registry.rs b/src/parsing/registry.rs index 9a3fda11..87463dac 100644 --- a/src/parsing/registry.rs +++ b/src/parsing/registry.rs @@ -392,6 +392,7 @@ fn initialize_registry(registry: &mut LanguageRegistry) { super::clojure::register(registry); super::lua::register(registry); super::swift::register(registry); + super::svelte::register(registry); } /// Get the global registry diff --git a/src/parsing/svelte/audit.rs b/src/parsing/svelte/audit.rs new file mode 100644 index 00000000..108f767c --- /dev/null +++ b/src/parsing/svelte/audit.rs @@ -0,0 +1,250 @@ +//! Svelte parser audit module +//! +//! Tracks which AST nodes the parser handles vs what's available in the grammar. +//! +//! Svelte symbol extraction is split: ` + +{#snippet card(item)} +
    {item}
    +{/snippet} +"#; + + let audit = SvelteParserAudit::audit_code(code).unwrap(); + + assert!(audit.grammar_nodes.contains_key("script_element")); + assert!(audit.grammar_nodes.contains_key("snippet_statement")); + + // Script body delegates to TS: greet is a Function. + assert!(audit.extracted_symbol_kinds.contains("Function")); + + // Svelte-level nodes the parser acts on are registered. + assert!(audit.implemented_nodes.contains("script_element")); + assert!(audit.implemented_nodes.contains("snippet_statement")); + } + + #[test] + fn test_template_node_names() { + let code = r#"{#if ready} +

    ok

    +{/if} +{#each items as item} + {@render row(item)} +{/each} +"#; + + let audit = SvelteParserAudit::audit_code(code).unwrap(); + + assert!(audit.grammar_nodes.contains_key("if_statement")); + assert!(audit.grammar_nodes.contains_key("each_statement")); + assert!(audit.grammar_nodes.contains_key("render_tag")); + } +} diff --git a/src/parsing/svelte/behavior.rs b/src/parsing/svelte/behavior.rs new file mode 100644 index 00000000..a9645926 --- /dev/null +++ b/src/parsing/svelte/behavior.rs @@ -0,0 +1,66 @@ +//! Svelte language behavior + +use super::resolution::{SvelteInheritanceResolver, SvelteResolutionContext}; +use crate::parsing::{InheritanceResolver, LanguageBehavior, ResolutionScope}; +use crate::{FileId, Visibility}; +use tree_sitter::Language; + +pub struct SvelteBehavior; + +impl SvelteBehavior { + pub fn new() -> Self { + Self + } +} + +impl Default for SvelteBehavior { + fn default() -> Self { + Self::new() + } +} + +impl LanguageBehavior for SvelteBehavior { + fn language_id(&self) -> crate::parsing::registry::LanguageId { + crate::parsing::registry::LanguageId::new("svelte") + } + + fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String { + base_path.to_string() + } + + fn module_separator(&self) -> &'static str { + "." + } + + fn source_roots(&self) -> &'static [&'static str] { + &["src", "lib", "routes", "components", "pages"] + } + + fn format_path_as_module(&self, components: &[&str]) -> Option { + if components.is_empty() { + None + } else { + Some(components.join(".")) + } + } + + fn get_language(&self) -> Language { + tree_sitter_svelte_next::LANGUAGE.into() + } + + fn parse_visibility(&self, signature: &str) -> Visibility { + if signature.contains("export ") { + Visibility::Public + } else { + Visibility::Private + } + } + + fn create_resolution_context(&self, file_id: FileId) -> Box { + Box::new(SvelteResolutionContext::new(file_id)) + } + + fn create_inheritance_resolver(&self) -> Box { + Box::new(SvelteInheritanceResolver::new()) + } +} diff --git a/src/parsing/svelte/definition.rs b/src/parsing/svelte/definition.rs new file mode 100644 index 00000000..2d35ac38 --- /dev/null +++ b/src/parsing/svelte/definition.rs @@ -0,0 +1,50 @@ +//! Svelte language definition and registration + +use crate::parsing::{ + LanguageBehavior, LanguageDefinition, LanguageId, LanguageParser, LanguageRegistry, +}; +use crate::{IndexError, IndexResult, Settings}; +use std::sync::Arc; + +use super::{SvelteBehavior, SvelteParser}; + +pub struct SvelteLanguage; + +impl LanguageDefinition for SvelteLanguage { + fn id(&self) -> LanguageId { + LanguageId::new("svelte") + } + + fn name(&self) -> &'static str { + "Svelte" + } + + fn extensions(&self) -> &'static [&'static str] { + &["svelte"] + } + + fn create_parser(&self, _settings: &Settings) -> IndexResult> { + let parser = SvelteParser::new().map_err(|e| IndexError::General(e.to_string()))?; + Ok(Box::new(parser)) + } + + fn create_behavior(&self) -> Box { + Box::new(SvelteBehavior::new()) + } + + fn default_enabled(&self) -> bool { + true + } + + fn is_enabled(&self, settings: &Settings) -> bool { + settings + .languages + .get("svelte") + .map(|config| config.enabled) + .unwrap_or(self.default_enabled()) + } +} + +pub(crate) fn register(registry: &mut LanguageRegistry) { + registry.register(Arc::new(SvelteLanguage)); +} diff --git a/src/parsing/svelte/mod.rs b/src/parsing/svelte/mod.rs new file mode 100644 index 00000000..7a633029 --- /dev/null +++ b/src/parsing/svelte/mod.rs @@ -0,0 +1,30 @@ +//! Svelte language parser implementation +//! +//! Svelte files are HTML-shaped templates whose ` + +

    Hello

    +"#; + let symbols = parser.parse(code, file_id(), &mut counter); + assert!( + symbols.iter().any(|s| s.name.as_ref() == "greet"), + "should extract greet function; got: {:?}", + symbols.iter().map(|s| s.name.as_ref()).collect::>() + ); + } + + #[test] + fn test_snippet_symbols() { + let mut parser = SvelteParser::new().unwrap(); + let mut counter = SymbolCounter::new(); + let code = r#" + +{#snippet card(item)} +
    {item.name}
    +{/snippet} +"#; + let symbols = parser.parse(code, file_id(), &mut counter); + assert!( + symbols.iter().any(|s| s.name.as_ref() == "card"), + "should extract snippet 'card'; got: {:?}", + symbols.iter().map(|s| s.name.as_ref()).collect::>() + ); + } + + #[test] + fn test_range_offset() { + // Script starts at line 1 (0-indexed), col 0 + let r = Range::new(0, 4, 0, 9); // line 0, col 4-9 in script + let offset = SvelteParser::offset_range(r, 1, 0); + assert_eq!(offset.start_line, 1); + assert_eq!(offset.start_column, 4); + + // Multi-line symbol: line 2 in script → line 3 in file + let r2 = Range::new(2, 0, 4, 1); + let offset2 = SvelteParser::offset_range(r2, 1, 0); + assert_eq!(offset2.start_line, 3); + assert_eq!(offset2.end_line, 5); + } + + #[test] + fn test_find_imports() { + let mut parser = SvelteParser::new().unwrap(); + let code = r#" + +"#; + let fid = file_id(); + let imports = parser.find_imports(code, fid); + assert!( + imports.iter().any(|i| i.path.contains("math")), + "should find math import; got: {:?}", + imports.iter().map(|i| &i.path).collect::>() + ); + } + + #[test] + fn test_typescript_script_symbols() { + // Svelte 5 defaults to `lang="ts"`; symbols must route through the TS parser. + let mut parser = SvelteParser::new().unwrap(); + let mut counter = SymbolCounter::new(); + let code = r#" + +

    Hello

    +"#; + let symbols = parser.parse(code, file_id(), &mut counter); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_ref()).collect(); + assert!( + names.contains(&"greet"), + "should extract greet function from TS block; got: {names:?}" + ); + assert!( + names.contains(&"User"), + "should extract User interface (TS-only construct); got: {names:?}" + ); + } + + #[test] + fn test_module_and_instance_scripts() { + // Both the module ` + + + +

    {VERSION}

    +"#; + let symbols = parser.parse(code, file_id(), &mut counter); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_ref()).collect(); + assert!( + names.contains(&"VERSION"), + "should extract VERSION from module script; got: {names:?}" + ); + assert!( + names.contains(&"start"), + "should extract start from instance script; got: {names:?}" + ); + } + + #[test] + fn test_typescript_imports() { + let mut parser = SvelteParser::new().unwrap(); + let code = r#" +"#; + let imports = parser.find_imports(code, file_id()); + let paths: Vec<&String> = imports.iter().map(|i| &i.path).collect(); + assert!( + paths.iter().any(|p| p.contains("api")), + "should find api import from TS block; got: {paths:?}" + ); + } +} diff --git a/src/parsing/svelte/resolution.rs b/src/parsing/svelte/resolution.rs new file mode 100644 index 00000000..e96f5e74 --- /dev/null +++ b/src/parsing/svelte/resolution.rs @@ -0,0 +1,181 @@ +//! Svelte-specific resolution and inheritance implementation. +//! +//! Svelte components hold JavaScript or TypeScript inside their `\n

    {x}

    \n", + &node_categories(), + |path| { + let audit = SvelteParserAudit::audit_file(path).map_err(|e| e.to_string())?; + let report = audit.generate_report(); + Ok(( + AuditData::new( + audit.grammar_nodes, + audit.implemented_nodes, + audit.extracted_symbol_kinds, + ), + report, + )) + }, + ); +}