Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-svelte-control-flow-use-import-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9098](https://github.com/biomejs/biome/issues/9098): `useImportType` no longer incorrectly flags imports used in Svelte control flow blocks (`{#if}`, `{#each}`, `{#await}`, `{#key}`) as type-only imports.
77 changes: 73 additions & 4 deletions crates/biome_cli/tests/cases/handle_svelte_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -873,16 +873,16 @@ let isChecked = false;
<main>
<!-- bind: directive -->
<input bind:value={inputValue} />

<!-- bind: directive with checkbox -->
<input type="checkbox" bind:checked={isChecked} />

<!-- class: directive -->
<div class:active={isActive}>Active</div>

<!-- style: directive -->
<div style:color={color}>Styled</div>

<!-- Using variables in text expressions -->
<p>{inputValue}</p>
<p>{isChecked}</p>
Expand Down Expand Up @@ -957,3 +957,72 @@ fn no_comma_operator_triggered_in_svelte_template_expression() {
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#"<script lang="ts">
import { IfEnum, ElseIfEnum, EachEnum, EachKeyEnum, KeyEnum, AwaitEnum } from './models.ts';

interface Props {
foo: IfEnum;
bar: ElseIfEnum;
baz: EachEnum;
bap: EachKeyEnum;
qux: KeyEnum;
zap: AwaitEnum;
}
let { foo }: Props = $props();
</script>

{#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}
<Component />
{/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,
));
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@ let isChecked = false;
<main>
<!-- bind: directive -->
<input bind:value={inputValue} />

<!-- bind: directive with checkbox -->
<input type="checkbox" bind:checked={isChecked} />

<!-- class: directive -->
<div class:active={isActive}>Active</div>

<!-- style: directive -->
<div style:color={color}>Styled</div>

<!-- Using variables in text expressions -->
<p>{inputValue}</p>
<p>{isChecked}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `biome.json`

```json
{
"html": {
"linter": { "enabled": true },
"experimentalFullSupportEnabled": true
}
}
```

## `file.svelte`

```svelte
<script lang="ts">
import { IfEnum, ElseIfEnum, EachEnum, EachKeyEnum, KeyEnum, AwaitEnum } from './models.ts';

interface Props {
foo: IfEnum;
bar: ElseIfEnum;
baz: EachEnum;
bap: EachKeyEnum;
qux: KeyEnum;
zap: AwaitEnum;
}
let { foo }: Props = $props();
</script>

{#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}
<Component />
{/key}

{#await AwaitEnum.Foo}
loading
{:then data}
{data}
{/await}

```

# Emitted Messages

```block
Checked 1 file in <TIME>. No fixes applied.
```
88 changes: 87 additions & 1 deletion crates/biome_service/src/file_handlers/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ use biome_html_syntax::{
AnySvelteDirective, AstroEmbeddedContent, HtmlAttributeInitializerClause,
HtmlDoubleTextExpression, HtmlElement, HtmlFileSource, HtmlLanguage, HtmlRoot,
HtmlSingleTextExpression, HtmlSyntaxNode, HtmlTextExpression, HtmlTextExpressions, HtmlVariant,
VueDirective, VueVBindShorthandDirective, VueVOnShorthandDirective, VueVSlotShorthandDirective,
SvelteAwaitBlock, SvelteEachBlock, SvelteIfBlock, SvelteKeyBlock, VueDirective,
VueVBindShorthandDirective, VueVOnShorthandDirective, VueVSlotShorthandDirective,
};
use biome_js_parser::parse_js_with_offset_and_cache;
use biome_js_syntax::{EmbeddingKind, JsFileSource, JsLanguage};
Expand Down Expand Up @@ -763,6 +764,91 @@ fn parse_embedded_nodes(
}
}

// Parse Svelte control flow block expressions ({#if}, {#each}, {#await}, {#key})
for element in html_root.syntax().descendants() {
let file_source = embedded_file_source
.with_embedding_kind(EmbeddingKind::Svelte { is_source: false });

// Handle {#if expression}
if let Some(if_block) = SvelteIfBlock::cast_ref(&element)
&& let Ok(opening_block) = if_block.opening_block()
&& let Ok(expression) = opening_block.expression()
&& let Some((content, doc_source)) =
parse_text_expression(expression, cache, biome_path, settings, file_source)
{
nodes.push((content.into(), doc_source));
}

// Handle {:else if expression}
if let Some(if_block) = SvelteIfBlock::cast_ref(&element) {
for else_if_clause in if_block.else_if_clauses() {
if let Ok(expression) = else_if_clause.expression()
&& let Some((content, doc_source)) = parse_text_expression(
expression,
cache,
biome_path,
settings,
file_source,
)
{
nodes.push((content.into(), doc_source));
}
}
}

// Handle {#each expression as item}
if let Some(each_block) = SvelteEachBlock::cast_ref(&element)
&& let Ok(opening_block) = each_block.opening_block()
{
if let Ok(expression) = opening_block.list()
&& let Some((content, doc_source)) = parse_text_expression(
expression,
cache,
biome_path,
settings,
file_source,
)
{
nodes.push((content.into(), doc_source));
}

if let Some(item) = opening_block.item()
&& let Some(item) = item.as_svelte_each_as_keyed_item()
&& let Some(key) = item.key()
&& let Ok(key_expression) = key.expression()
&& let Some((content, doc_source)) = parse_text_expression(
key_expression,
cache,
biome_path,
settings,
file_source,
)
{
nodes.push((content.into(), doc_source));
}
}

// Handle {#await expression}
if let Some(await_block) = SvelteAwaitBlock::cast_ref(&element)
&& let Ok(opening_block) = await_block.opening_block()
&& let Ok(expression) = opening_block.expression()
&& let Some((content, doc_source)) =
parse_text_expression(expression, cache, biome_path, settings, file_source)
{
nodes.push((content.into(), doc_source));
}

// Handle {#key expression}
if let Some(key_block) = SvelteKeyBlock::cast_ref(&element)
&& let Ok(opening_block) = key_block.opening_block()
&& let Ok(expression) = opening_block.expression()
&& let Some((content, doc_source)) =
parse_text_expression(expression, cache, biome_path, settings, file_source)
{
nodes.push((content.into(), doc_source));
}
}

// Parse Svelte directive attributes (bind:, class:, use:, etc.)
// Note: on: event handlers are legacy Svelte 3/4 syntax and not supported.
// Svelte 5 runes mode uses regular attributes for event handlers.
Expand Down