Skip to content

Commit 4ff83dd

Browse files
fix(vue): slots are now parsed with a new root (#12030)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent aaf4c81 commit 4ff83dd

49 files changed

Lines changed: 1024 additions & 19 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/ten-queens-enter.md‎

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 [#9155](https://github.com/biomejs/biome/issues/9155): Biome no longer reports a parse error for typed slot props in Vue files, such as `v-slot="{ value }: { value: ValueType }"`. Types used in slot props annotations are now correctly detected as used by [`noUnusedVariables`](https://biomejs.dev/linter/rules/no-unused-variables/).

‎crates/biome_cli/tests/cases/handle_vue_files.rs‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,3 +1228,50 @@ import { mdiSquareOutline } from "@mdi/js";
12281228
result,
12291229
));
12301230
}
1231+
1232+
#[test]
1233+
fn typed_slot_props_are_parsed_and_formatted() {
1234+
let fs = MemoryFileSystem::default();
1235+
let mut console = BufferConsole::default();
1236+
1237+
fs.insert(
1238+
"biome.json".into(),
1239+
r#"{ "html": { "formatter": {"enabled": true}, "linter": {"enabled": true}, "experimentalFullSupportEnabled": true } }"#.as_bytes(),
1240+
);
1241+
1242+
let vue_file_path = Utf8Path::new("file.vue");
1243+
fs.insert(
1244+
vue_file_path.into(),
1245+
r#"<template>
1246+
<Component v-slot="{value}:{value:ValueType}">{{ value.innerValue }}</Component>
1247+
<Component #item="{ item=fallback }:{ item?: ItemType }">{{ item }}</Component>
1248+
</template>
1249+
<script lang="ts" setup>
1250+
import Component from "./Component.vue";
1251+
1252+
interface ValueType {
1253+
innerValue: string;
1254+
}
1255+
type ItemType = string;
1256+
const fallback: ItemType = "fallback";
1257+
</script>
1258+
"#
1259+
.as_bytes(),
1260+
);
1261+
1262+
let (fs, result) = run_cli(
1263+
fs,
1264+
&mut console,
1265+
Args::from(["check", "--write", vue_file_path.as_str()].as_slice()),
1266+
);
1267+
1268+
assert!(result.is_ok(), "run_cli returned {result:?}");
1269+
1270+
assert_cli_snapshot(SnapshotPayload::new(
1271+
module_path!(),
1272+
"typed_slot_props_are_parsed_and_formatted",
1273+
fs,
1274+
console,
1275+
result,
1276+
));
1277+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
source: crates/biome_cli/tests/snap_test.rs
3+
expression: redactor(content)
4+
---
5+
## Command
6+
7+
```shell
8+
biome check --write file.vue
9+
```
10+
11+
## `biome.json`
12+
13+
```json
14+
{
15+
"html": {
16+
"formatter": { "enabled": true },
17+
"linter": { "enabled": true },
18+
"experimentalFullSupportEnabled": true
19+
}
20+
}
21+
```
22+
23+
## `file.vue`
24+
25+
```vue
26+
<template>
27+
<Component v-slot="{value}:{value:ValueType}">{{
28+
value.innerValue
29+
}}</Component>
30+
<Component #item="{ item=fallback }:{ item?: ItemType }">{{
31+
item
32+
}}</Component>
33+
</template>
34+
<script lang="ts" setup>
35+
import Component from "./Component.vue";
36+
37+
interface ValueType {
38+
innerValue: string;
39+
}
40+
type ItemType = string;
41+
const fallback: ItemType = "fallback";
42+
</script>
43+
44+
```
45+
46+
# Emitted Messages
47+
48+
```block
49+
Checked 1 file in <TIME>. Fixed 1 file.
50+
```

‎crates/biome_embeds/src/references.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ mod tests {
277277
is_source: false,
278278
event_handler: false,
279279
allow_statements: false,
280+
slot_props: false,
280281
},
281282
)),
282283
})
@@ -299,6 +300,7 @@ mod tests {
299300
is_source: false,
300301
event_handler: false,
301302
allow_statements: false,
303+
slot_props: false,
302304
}),
303305
JsParserOptions::default(),
304306
)
@@ -331,6 +333,7 @@ mod tests {
331333
is_source: false,
332334
event_handler: false,
333335
allow_statements: false,
336+
slot_props: false,
334337
}),
335338
JsParserOptions::default(),
336339
)

‎crates/biome_embeds/src/visitor.rs‎

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ use biome_js_syntax::{
1414
AnyJsAssignmentPattern, AnyJsBindingPattern, AnyJsCallArgument, AnyJsDeclarationClause,
1515
AnyJsExportClause, AnyJsExpression, AnyJsIdentifierReference, AnyJsModuleItem,
1616
AnyJsObjectAssignmentPatternMember, AnyJsObjectBindingPatternMember, AnyJsObjectMember,
17-
AnyJsRoot, AnyJsStatement, AnyTsIdentifierBinding, AnyTsType, JsAssignmentExpression,
18-
JsCallExpression, JsExport, JsIdentifierAssignment, JsImport, JsModuleItemList,
19-
JsReferenceIdentifier, JsStaticMemberExpression, JsSvelteDeclarationRoot, JsSvelteSnippetRoot,
20-
JsVariableStatement, JsxReferenceIdentifier,
17+
AnyJsParameter, AnyJsRoot, AnyJsStatement, AnyTsIdentifierBinding, AnyTsType,
18+
JsAssignmentExpression, JsCallExpression, JsExport, JsIdentifierAssignment, JsImport,
19+
JsModuleItemList, JsParameterList, JsReferenceIdentifier, JsStaticMemberExpression,
20+
JsSvelteDeclarationRoot, JsSvelteSnippetRoot, JsVariableStatement, JsxReferenceIdentifier,
2121
};
2222
use biome_languages::html::HtmlVariant;
2323
use biome_languages::javascript::{JsEmbeddingKind, SvelteEmbeddingKind};
@@ -488,10 +488,9 @@ impl EmbeddedBindingsBuilder {
488488
&& let Some(root) = snippet
489489
.parse
490490
.tree::<AnyJsRoot>()
491-
.as_js_expression_template_root()
492-
&& let Some(expression) = root.expression()
491+
.as_js_vue_slot_props_root()
493492
{
494-
self.visit_expression_bindings(&expression);
493+
self.visit_parameter_list_bindings(&root.parameters());
495494
}
496495
}
497496
}
@@ -787,6 +786,21 @@ impl EmbeddedBindingsBuilder {
787786
None
788787
}
789788

789+
fn visit_parameter_list_bindings(&mut self, parameters: &JsParameterList) {
790+
for parameter in parameters.iter().flatten() {
791+
let binding = match parameter {
792+
AnyJsParameter::AnyJsFormalParameter(parameter) => parameter
793+
.as_js_formal_parameter()
794+
.and_then(|parameter| parameter.binding().ok()),
795+
AnyJsParameter::JsRestParameter(parameter) => parameter.binding().ok(),
796+
AnyJsParameter::TsThisParameter(_) => None,
797+
};
798+
if let Some(binding) = binding {
799+
self.visit_any_js_binding_pattern(&binding);
800+
}
801+
}
802+
}
803+
790804
fn visit_svelte_declaration(
791805
&mut self,
792806
root: &JsSvelteDeclarationRoot,

‎crates/biome_grit_patterns/src/grit_target_language/js_target_language/generated_mappings.rs‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!-- should not generate diagnostics -->
2+
<template>
3+
<div>
4+
<template #default="{ value }: { value: string }">{{ value }}</template>
5+
<template v-slot:header="{ title: heading, ...rest }: HeaderProps">
6+
{{ heading }} {{ rest }}
7+
</template>
8+
<template #items="[first, ...tail]: string[]">{{ first }} {{ tail }}</template>
9+
</div>
10+
</template>
11+
<script lang="ts" setup>
12+
type HeaderProps = { title: string; subtitle: string };
13+
</script>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: valid-vue-slot-typed-props.vue
4+
---
5+
# Input
6+
```vue
7+
<!-- should not generate diagnostics -->
8+
<template>
9+
<div>
10+
<template #default="{ value }: { value: string }">{{ value }}</template>
11+
<template v-slot:header="{ title: heading, ...rest }: HeaderProps">
12+
{{ heading }} {{ rest }}
13+
</template>
14+
<template #items="[first, ...tail]: string[]">{{ first }} {{ tail }}</template>
15+
</div>
16+
</template>
17+
<script lang="ts" setup>
18+
type HeaderProps = { title: string; subtitle: string };
19+
</script>
20+
21+
```
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!-- should generate diagnostics -->
2+
<script setup lang="ts">
3+
import Component from "./Component.vue";
4+
5+
interface ValueType {
6+
innerValue: string;
7+
}
8+
interface UnusedType {
9+
innerValue: string;
10+
}
11+
</script>
12+
13+
<template>
14+
<Component v-slot="{ value }: { value: ValueType }">{{ value.innerValue }}</Component>
15+
</template>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid-vue-slot-typed-props.vue
4+
---
5+
# Input
6+
```vue
7+
<!-- should generate diagnostics -->
8+
<script setup lang="ts">
9+
import Component from "./Component.vue";
10+
11+
interface ValueType {
12+
innerValue: string;
13+
}
14+
interface UnusedType {
15+
innerValue: string;
16+
}
17+
</script>
18+
19+
<template>
20+
<Component v-slot="{ value }: { value: ValueType }">{{ value.innerValue }}</Component>
21+
</template>
22+
23+
```
24+
25+
# Diagnostics
26+
```
27+
invalid-vue-slot-typed-props.vue:8:11 lint/correctness/noUnusedVariables ━━━━━━━━━━━━━━━━━━━━━━━━━━━
28+
29+
! This interface UnusedType is unused.
30+
31+
6 │ innerValue: string;
32+
7 │ }
33+
> 8 │ interface UnusedType {
34+
│ ^^^^^^^^^^
35+
9 │ innerValue: string;
36+
10 │ }
37+
38+
i Unused variables are often the result of typos, incomplete refactors, or other sources of bugs.
39+
40+
41+
```

0 commit comments

Comments
 (0)