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/no-assign-in-expressions-vue-v-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Fixed [#9161](https://github.com/biomejs/biome/issues/9161): The `noAssignInExpressions` rule no longer flags assignments in Vue v-on directives (e.g., `@click="counter += 1"`). Assignments in event handlers are idiomatic Vue patterns and are now skipped by the rule.
73 changes: 73 additions & 0 deletions crates/biome_cli/tests/cases/handle_vue_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1614,3 +1614,76 @@ fn no_comma_operator_not_triggered_in_v_for() {
result,
));
}

#[test]
fn no_assign_in_expressions_not_triggered_in_v_on() {
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.vue");
fs.insert(
file.into(),
r#"<script setup>
let counter = 0;
</script>
<template>
<!-- shorthand @click -->
<button type="button" @click="counter += 1">+</button>
<!-- longhand v-on:click -->
<button type="button" v-on:click="counter -= 1">-</button>
</template>"#
.as_bytes(),
);
let (fs, result) = run_cli(
fs,
&mut console,
Args::from(["lint", "--only=noAssignInExpressions", file.as_str()].as_slice()),
);
assert!(result.is_ok(), "run_cli returned {result:?}");
assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"no_assign_in_expressions_not_triggered_in_v_on",
fs,
console,
result,
));
}

#[test]
fn no_assign_in_expressions_triggered_in_template_interpolation() {
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.vue");
fs.insert(
file.into(),
r#"<script setup>
let counter = 0;
</script>
<template>
<p>{{ counter += 1 }}</p>
</template>"#
.as_bytes(),
);
let (fs, result) = run_cli(
fs,
&mut console,
Args::from(["lint", "--only=noAssignInExpressions", file.as_str()].as_slice()),
);
assert!(result.is_err(), "run_cli returned {result:?}");
assert_cli_snapshot(SnapshotPayload::new(
module_path!(),
"no_assign_in_expressions_triggered_in_template_interpolation",
fs,
console,
result,
));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `biome.json`

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

## `file.vue`

```vue
<script setup>
let counter = 0;
</script>
<template>
<!-- shorthand @click -->
<button type="button" @click="counter += 1">+</button>
<!-- longhand v-on:click -->
<button type="button" v-on:click="counter -= 1">-</button>
</template>
```

# Emitted Messages

```block
Checked 1 file in <TIME>. No fixes applied.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
source: crates/biome_cli/tests/snap_test.rs
expression: redactor(content)
---
## `biome.json`

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

## `file.vue`

```vue
<script setup>
let counter = 0;
</script>
<template>
<p>{{ counter += 1 }}</p>
</template>
```

# Termination Message

```block
lint ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× Some errors were emitted while running checks.



```

# Emitted Messages

```block
file.vue:5:9 lint/suspicious/noAssignInExpressions ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

× The assignment should not be in an expression.

3 │ </script>
4 │ <template>
> 5 │ <p>{{ counter += 1 }}</p>
│ ^^^^^^^^^^^^
6 │ </template>

i The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.


```

```block
Checked 1 file in <TIME>. No fixes applied.
Found 1 error.
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use biome_console::markup;
use biome_diagnostics::Severity;
use biome_js_syntax::{
AnyJsFunctionBody, JsArrowFunctionExpression, JsAssignmentExpression, JsExpressionStatement,
JsForStatement, JsParenthesizedExpression, JsSequenceExpression,
JsFileSource, JsForStatement, JsParenthesizedExpression, JsSequenceExpression,
};
use biome_rowan::AstNode;
use biome_rule_options::no_assign_in_expressions::NoAssignInExpressionsOptions;
Expand Down Expand Up @@ -65,6 +65,13 @@ impl Rule for NoAssignInExpressions {
type Options = NoAssignInExpressionsOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
// Skip assignments in Vue event handlers (v-on directives)
// These are idiomatic Vue patterns, not accidental assignments
let file_source = ctx.source_type::<JsFileSource>();
if file_source.is_vue_event_handler() {
return None;
}

let assign = ctx.query();
let mut ancestor = assign
.syntax()
Expand Down
18 changes: 18 additions & 0 deletions crates/biome_js_syntax/src/file_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ pub enum EmbeddingKind {
setup: bool,
/// Where the bindings are defined
is_source: bool,
/// Whether this is a v-on event handler (e.g., @click="handler")
event_handler: bool,
},
Svelte {
/// Where the bindings are defined
Expand All @@ -151,6 +153,15 @@ impl EmbeddingKind {
pub const fn is_vue_setup(&self) -> bool {
matches!(self, Self::Vue { setup: true, .. })
}
pub const fn is_vue_event_handler(&self) -> bool {
matches!(
self,
Self::Vue {
event_handler: true,
..
}
)
}
pub const fn is_svelte(&self) -> bool {
matches!(self, Self::Svelte { .. })
}
Expand Down Expand Up @@ -226,6 +237,7 @@ impl JsFileSource {
Self::js_module().with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: true,
event_handler: false,
})
}

Expand All @@ -234,6 +246,7 @@ impl JsFileSource {
Self::js_module().with_embedding_kind(EmbeddingKind::Vue {
setup: true,
is_source: true,
event_handler: false,
})
}

Expand Down Expand Up @@ -338,6 +351,11 @@ impl JsFileSource {
)
}

/// Returns true if this is a Vue event handler (v-on directive)
pub const fn is_vue_event_handler(&self) -> bool {
self.embedding_kind.is_vue_event_handler()
}

pub const fn as_embedding_kind(&self) -> &EmbeddingKind {
&self.embedding_kind
}
Expand Down
10 changes: 10 additions & 0 deletions crates/biome_service/src/file_handlers/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ fn parse_embedded_nodes(
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
event_handler: true,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
Expand All @@ -647,6 +648,7 @@ fn parse_embedded_nodes(
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
event_handler: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
Expand All @@ -667,6 +669,7 @@ fn parse_embedded_nodes(
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
event_handler: false,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
Expand All @@ -683,10 +686,15 @@ fn parse_embedded_nodes(
if let Some(directive) = VueDirective::cast_ref(&element)
&& let Some(initializer) = directive.initializer()
{
let is_v_on = directive
.name_token()
.map(|t| t.text_trimmed() == "v-on")
.unwrap_or(false);
let file_source =
embedded_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
event_handler: is_v_on,
});
if let Some((content, doc_source)) = parse_directive_string_value(
&initializer,
Expand Down Expand Up @@ -934,6 +942,7 @@ pub(crate) fn parse_embedded_script(
file_source = file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: element.is_script_with_setup_attribute(),
is_source: true,
event_handler: false,
});
}
file_source
Expand Down Expand Up @@ -1195,6 +1204,7 @@ pub(crate) fn parse_vue_text_expression(
let file_source = js_file_source.with_embedding_kind(EmbeddingKind::Vue {
setup: false,
is_source: false,
event_handler: false,
});
parse_text_expression(expression, cache, biome_path, settings, file_source)
}
Expand Down
1 change: 1 addition & 0 deletions crates/biome_service/src/file_handlers/vue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl VueFileHandler {
.with_embedding_kind(EmbeddingKind::Vue {
setup,
is_source: true,
event_handler: false,
}),
)
})
Expand Down
4 changes: 4 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading