Skip to content

Commit fc9850c

Browse files
authored
fix(service): parse text expressions in svelte control flow blocks (#9103)
1 parent 0c0fb6f commit fc9850c

5 files changed

Lines changed: 228 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
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.

crates/biome_cli/tests/cases/handle_svelte_files.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -873,16 +873,16 @@ let isChecked = false;
873873
<main>
874874
<!-- bind: directive -->
875875
<input bind:value={inputValue} />
876-
876+
877877
<!-- bind: directive with checkbox -->
878878
<input type="checkbox" bind:checked={isChecked} />
879-
879+
880880
<!-- class: directive -->
881881
<div class:active={isActive}>Active</div>
882-
882+
883883
<!-- style: directive -->
884884
<div style:color={color}>Styled</div>
885-
885+
886886
<!-- Using variables in text expressions -->
887887
<p>{inputValue}</p>
888888
<p>{isChecked}</p>
@@ -957,3 +957,72 @@ fn no_comma_operator_triggered_in_svelte_template_expression() {
957957
result,
958958
));
959959
}
960+
961+
#[test]
962+
fn use_import_type_not_triggered_for_enum_in_control_flow_blocks() {
963+
let fs = MemoryFileSystem::default();
964+
let mut console = BufferConsole::default();
965+
966+
fs.insert(
967+
"biome.json".into(),
968+
r#"{ "html": { "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#
969+
.as_bytes(),
970+
);
971+
972+
let file = Utf8Path::new("file.svelte");
973+
// 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
974+
fs.insert(
975+
file.into(),
976+
r#"<script lang="ts">
977+
import { IfEnum, ElseIfEnum, EachEnum, EachKeyEnum, KeyEnum, AwaitEnum } from './models.ts';
978+
979+
interface Props {
980+
foo: IfEnum;
981+
bar: ElseIfEnum;
982+
baz: EachEnum;
983+
bap: EachKeyEnum;
984+
qux: KeyEnum;
985+
zap: AwaitEnum;
986+
}
987+
let { foo }: Props = $props();
988+
</script>
989+
990+
{#if foo === IfEnum.private}
991+
private
992+
{:else if foo === ElseIfEnum.public}
993+
public
994+
{/if}
995+
996+
{#each EachEnum.Foo as item (EachKeyEnum[item])}
997+
{item.name}
998+
{/each}
999+
1000+
{#key KeyEnum.Foo}
1001+
<Component />
1002+
{/key}
1003+
1004+
{#await AwaitEnum.Foo}
1005+
loading
1006+
{:then data}
1007+
{data}
1008+
{/await}
1009+
"#
1010+
.as_bytes(),
1011+
);
1012+
1013+
let (fs, result) = run_cli(
1014+
fs,
1015+
&mut console,
1016+
Args::from(["lint", "--only=useImportType", file.as_str()].as_slice()),
1017+
);
1018+
1019+
assert!(result.is_ok(), "run_cli returned {result:?}");
1020+
1021+
assert_cli_snapshot(SnapshotPayload::new(
1022+
module_path!(),
1023+
"use_import_type_not_triggered_for_enum_in_control_flow_blocks",
1024+
fs,
1025+
console,
1026+
result,
1027+
));
1028+
}

crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/no_unused_variables_in_svelte_directives.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,16 @@ let isChecked = false;
2626
<main>
2727
<!-- bind: directive -->
2828
<input bind:value={inputValue} />
29-
29+
3030
<!-- bind: directive with checkbox -->
3131
<input type="checkbox" bind:checked={isChecked} />
32-
32+
3333
<!-- class: directive -->
3434
<div class:active={isActive}>Active</div>
35-
35+
3636
<!-- style: directive -->
3737
<div style:color={color}>Styled</div>
38-
38+
3939
<!-- Using variables in text expressions -->
4040
<p>{inputValue}</p>
4141
<p>{isChecked}</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
source: crates/biome_cli/tests/snap_test.rs
3+
expression: redactor(content)
4+
---
5+
## `biome.json`
6+
7+
```json
8+
{
9+
"html": {
10+
"linter": { "enabled": true },
11+
"experimentalFullSupportEnabled": true
12+
}
13+
}
14+
```
15+
16+
## `file.svelte`
17+
18+
```svelte
19+
<script lang="ts">
20+
import { IfEnum, ElseIfEnum, EachEnum, EachKeyEnum, KeyEnum, AwaitEnum } from './models.ts';
21+
22+
interface Props {
23+
foo: IfEnum;
24+
bar: ElseIfEnum;
25+
baz: EachEnum;
26+
bap: EachKeyEnum;
27+
qux: KeyEnum;
28+
zap: AwaitEnum;
29+
}
30+
let { foo }: Props = $props();
31+
</script>
32+
33+
{#if foo === IfEnum.private}
34+
private
35+
{:else if foo === ElseIfEnum.public}
36+
public
37+
{/if}
38+
39+
{#each EachEnum.Foo as item (EachKeyEnum[item])}
40+
{item.name}
41+
{/each}
42+
43+
{#key KeyEnum.Foo}
44+
<Component />
45+
{/key}
46+
47+
{#await AwaitEnum.Foo}
48+
loading
49+
{:then data}
50+
{data}
51+
{/await}
52+
53+
```
54+
55+
# Emitted Messages
56+
57+
```block
58+
Checked 1 file in <TIME>. No fixes applied.
59+
```

crates/biome_service/src/file_handlers/html.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ use biome_html_syntax::{
4444
AnySvelteDirective, AstroEmbeddedContent, HtmlAttributeInitializerClause,
4545
HtmlDoubleTextExpression, HtmlElement, HtmlFileSource, HtmlLanguage, HtmlRoot,
4646
HtmlSingleTextExpression, HtmlSyntaxNode, HtmlTextExpression, HtmlTextExpressions, HtmlVariant,
47-
VueDirective, VueVBindShorthandDirective, VueVOnShorthandDirective, VueVSlotShorthandDirective,
47+
SvelteAwaitBlock, SvelteEachBlock, SvelteIfBlock, SvelteKeyBlock, VueDirective,
48+
VueVBindShorthandDirective, VueVOnShorthandDirective, VueVSlotShorthandDirective,
4849
};
4950
use biome_js_parser::parse_js_with_offset_and_cache;
5051
use biome_js_syntax::{EmbeddingKind, JsFileSource, JsLanguage};
@@ -763,6 +764,91 @@ fn parse_embedded_nodes(
763764
}
764765
}
765766

767+
// Parse Svelte control flow block expressions ({#if}, {#each}, {#await}, {#key})
768+
for element in html_root.syntax().descendants() {
769+
let file_source = embedded_file_source
770+
.with_embedding_kind(EmbeddingKind::Svelte { is_source: false });
771+
772+
// Handle {#if expression}
773+
if let Some(if_block) = SvelteIfBlock::cast_ref(&element)
774+
&& let Ok(opening_block) = if_block.opening_block()
775+
&& let Ok(expression) = opening_block.expression()
776+
&& let Some((content, doc_source)) =
777+
parse_text_expression(expression, cache, biome_path, settings, file_source)
778+
{
779+
nodes.push((content.into(), doc_source));
780+
}
781+
782+
// Handle {:else if expression}
783+
if let Some(if_block) = SvelteIfBlock::cast_ref(&element) {
784+
for else_if_clause in if_block.else_if_clauses() {
785+
if let Ok(expression) = else_if_clause.expression()
786+
&& let Some((content, doc_source)) = parse_text_expression(
787+
expression,
788+
cache,
789+
biome_path,
790+
settings,
791+
file_source,
792+
)
793+
{
794+
nodes.push((content.into(), doc_source));
795+
}
796+
}
797+
}
798+
799+
// Handle {#each expression as item}
800+
if let Some(each_block) = SvelteEachBlock::cast_ref(&element)
801+
&& let Ok(opening_block) = each_block.opening_block()
802+
{
803+
if let Ok(expression) = opening_block.list()
804+
&& let Some((content, doc_source)) = parse_text_expression(
805+
expression,
806+
cache,
807+
biome_path,
808+
settings,
809+
file_source,
810+
)
811+
{
812+
nodes.push((content.into(), doc_source));
813+
}
814+
815+
if let Some(item) = opening_block.item()
816+
&& let Some(item) = item.as_svelte_each_as_keyed_item()
817+
&& let Some(key) = item.key()
818+
&& let Ok(key_expression) = key.expression()
819+
&& let Some((content, doc_source)) = parse_text_expression(
820+
key_expression,
821+
cache,
822+
biome_path,
823+
settings,
824+
file_source,
825+
)
826+
{
827+
nodes.push((content.into(), doc_source));
828+
}
829+
}
830+
831+
// Handle {#await expression}
832+
if let Some(await_block) = SvelteAwaitBlock::cast_ref(&element)
833+
&& let Ok(opening_block) = await_block.opening_block()
834+
&& let Ok(expression) = opening_block.expression()
835+
&& let Some((content, doc_source)) =
836+
parse_text_expression(expression, cache, biome_path, settings, file_source)
837+
{
838+
nodes.push((content.into(), doc_source));
839+
}
840+
841+
// Handle {#key expression}
842+
if let Some(key_block) = SvelteKeyBlock::cast_ref(&element)
843+
&& let Ok(opening_block) = key_block.opening_block()
844+
&& let Ok(expression) = opening_block.expression()
845+
&& let Some((content, doc_source)) =
846+
parse_text_expression(expression, cache, biome_path, settings, file_source)
847+
{
848+
nodes.push((content.into(), doc_source));
849+
}
850+
}
851+
766852
// Parse Svelte directive attributes (bind:, class:, use:, etc.)
767853
// Note: on: event handlers are legacy Svelte 3/4 syntax and not supported.
768854
// Svelte 5 runes mode uses regular attributes for event handlers.

0 commit comments

Comments
 (0)