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