diff --git a/.changeset/tasty-wings-bake.md b/.changeset/tasty-wings-bake.md
new file mode 100644
index 000000000000..06314fc72d03
--- /dev/null
+++ b/.changeset/tasty-wings-bake.md
@@ -0,0 +1,5 @@
+---
+"@biomejs/biome": patch
+---
+
+Fixed [#6316](https://github.com/biomejs/biome/issues/6316): Biome now resolves Svelte `$store` references to the underlying `store` binding in semantic analysis, preventing false `noUndeclaredVariables` diagnostics when the store is declared.
diff --git a/crates/biome_cli/tests/cases/handle_svelte_files.rs b/crates/biome_cli/tests/cases/handle_svelte_files.rs
index df5a00040be9..de2742db361e 100644
--- a/crates/biome_cli/tests/cases/handle_svelte_files.rs
+++ b/crates/biome_cli/tests/cases/handle_svelte_files.rs
@@ -40,6 +40,38 @@ var foo: string = "";
"#;
+// In Svelte, `writable(...)` creates a store binding (`count`), and `$count` is the
+// auto-subscription syntax that reads the store value.
+// This test verifies `$count` resolves through `count`, while `$missing` stays undeclared.
+const SVELTE_STORE_DEREFERENCE_FILE: &str = r#"
+"#;
+
+const SVELTE_MODULE_STORE_DEREFERENCE_FILE: &str = r#"import { writable } from "svelte/store";
+const count = writable(1);
+$count;
+$missing;"#;
+
+const SVELTE_JS_MODULE_STORE_DEREFERENCE_FILE: &str = r#"import { writable } from "svelte/store";
+const count = writable(1);
+$count;
+$missing;"#;
+
+const SVELTE_MODULE_TYPE_ONLY_BINDING_DEREFERENCE_FILE: &str = r#"type count = number;
+$count;"#;
+
+// Regression guard for `.svelte` + `
+"#;
+
#[test]
fn sorts_imports_check() {
let fs = MemoryFileSystem::default();
@@ -542,3 +574,643 @@ fn check_stdin_write_unsafe_successfully() {
result,
));
}
+
+// `.svelte` component path: `$count` should resolve via extracted `
+
+
+ {hello}
+ {notDefined}
+ {#each array as item}
+ {/each}
+
+
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noUnusedVariables", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "embedded_bindings_are_tracked_correctly",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn use_const_not_triggered_in_snippet_sources() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+
+ {hello}
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=useConst", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "use_const_not_triggered_in_snippet_sources",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn no_unused_imports_is_not_triggered_in_snippet_sources() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noUnusedImports", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "no_unused_imports_is_not_triggered_in_snippet_sources",
+ fs,
+ console,
+ result,
+ ));
+}
+
+const SVELTE_ENUM_IN_TEMPLATE: &str = r#"
+
+
+ {FooEnum.Foo}
+"#;
+
+#[test]
+fn use_import_type_not_triggered_for_enum_in_template() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(file.into(), SVELTE_ENUM_IN_TEMPLATE.as_bytes());
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=useImportType", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "use_import_type_not_triggered_for_enum_in_template",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn use_import_type_not_triggered_for_enum_in_template_v2() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=useImportType", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "use_import_type_not_triggered_for_enum_in_template_v2",
+ fs,
+ console,
+ result,
+ ));
+}
+#[test]
+fn no_useless_lone_block_statements_is_not_triggered() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+ {#snippet child({ props, open })}
+ {#if open}
+
+ {@render children?.()}
+
+ {/if}
+ {/snippet}
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noUselessLoneBlockStatements", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "no_useless_lone_block_statements_is_not_triggered",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn supports_ts_in_embedded_expressions() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+ {
+ if ((e.target as HTMLElement).closest("button")) {
+ return;
+ }
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
+ }}
+>
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noUselessLoneBlockStatements", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "supports_ts_in_embedded_expressions",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn no_unused_variables_in_svelte_directives() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+
+
+
+
+
+
+
+
+ Active
+
+
+ Styled
+
+
+ {inputValue}
+ {isChecked}
+
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noUnusedVariables", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "no_unused_variables_in_svelte_directives",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn no_comma_operator_triggered_in_svelte_template_expression() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ fs.insert(
+ file.into(),
+ r#"
+
+
+{(console.log("side effect"), x)}
"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=noCommaOperator", file.as_str()].as_slice()),
+ );
+
+ // The comma operator SHOULD be flagged in Svelte (hack only applies to Vue)
+ // Result is Ok because it's a warning, but console should contain the diagnostic
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+ let has_comma_operator = console.out_buffer.iter().any(|m| {
+ let content = format!("{:?}", m.content);
+ content.contains("noCommaOperator")
+ });
+ assert!(
+ has_comma_operator,
+ "Expected noCommaOperator diagnostic in console output"
+ );
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "no_comma_operator_triggered_in_svelte_template_expression",
+ fs,
+ console,
+ result,
+ ));
+}
+
+#[test]
+fn use_import_type_not_triggered_for_enum_in_control_flow_blocks() {
+ let fs = MemoryFileSystem::default();
+ let mut console = BufferConsole::default();
+
+ fs.insert(
+ "biome.json".into(),
+ r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
+ .as_bytes(),
+ );
+
+ let file = Utf8Path::new("file.svelte");
+ // the code in this file is intentionally ridiculous and doesn't necessarily make sense, but it covers a lot of different control flow blocks in one test
+ fs.insert(
+ file.into(),
+ r#"
+
+{#if foo === IfEnum.private}
+ private
+{:else if foo === ElseIfEnum.public}
+ public
+{/if}
+
+{#each EachEnum.Foo as item (EachKeyEnum[item])}
+ {item.name}
+{/each}
+
+{#key KeyEnum.Foo}
+
+{/key}
+
+{#await AwaitEnum.Foo}
+ loading
+{:then data}
+ {data}
+{/await}
+"#
+ .as_bytes(),
+ );
+
+ let (fs, result) = run_cli(
+ fs,
+ &mut console,
+ Args::from(["lint", "--only=useImportType", file.as_str()].as_slice()),
+ );
+
+ assert!(result.is_ok(), "run_cli returned {result:?}");
+
+ assert_cli_snapshot(SnapshotPayload::new(
+ module_path!(),
+ "use_import_type_not_triggered_for_enum_in_control_flow_blocks",
+ fs,
+ console,
+ result,
+ ));
+}
diff --git a/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/no_undeclared_variables_check_types_handles_svelte_ts_script_globals.snap b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/no_undeclared_variables_check_types_handles_svelte_ts_script_globals.snap
new file mode 100644
index 000000000000..854a15a1ac29
--- /dev/null
+++ b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/no_undeclared_variables_check_types_handles_svelte_ts_script_globals.snap
@@ -0,0 +1,37 @@
+---
+source: crates/biome_cli/tests/snap_test.rs
+expression: redactor(content)
+---
+## `biome.json`
+
+```json
+{
+ "linter": {
+ "rules": {
+ "correctness": {
+ "noUndeclaredVariables": {
+ "level": "error",
+ "options": {
+ "checkTypes": true
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## `file.svelte`
+
+```svelte
+
+
+```
+
+# Emitted Messages
+
+```block
+Checked 1 file in