Skip to content

Commit be42745

Browse files
amousa11dyc3
andauthored
feat(analyze/js): add noVueDataObjectDeclaration (#6574)
Co-authored-by: Carson McManus <carson.mcmanus1@gmail.com>
1 parent 9f4538a commit be42745

75 files changed

Lines changed: 1147 additions & 429 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.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 83 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ define_categories! {
198198
"lint/nursery/noUselessUndefined": "https://biomejs.dev/linter/rules/no-useless-undefined",
199199
"lint/nursery/noVueReservedKeys": "https://biomejs.dev/linter/rules/no-vue-reserved-keys",
200200
"lint/nursery/noVueReservedProps": "https://biomejs.dev/linter/rules/no-vue-reserved-props",
201+
"lint/nursery/noVueDataObjectDeclaration": "https://biomejs.dev/linter/rules/no-vue-data-object-declaration",
201202
"lint/nursery/useAdjacentGetterSetter": "https://biomejs.dev/linter/rules/use-adjacent-getter-setter",
202203
"lint/nursery/useAnchorHref": "https://biomejs.dev/linter/rules/use-anchor-href",
203204
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",

crates/biome_js_analyze/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ smallvec = { workspace = true }
5151
biome_fs = { workspace = true }
5252
biome_js_parser = { path = "../biome_js_parser", features = ["tests"] }
5353
biome_plugin_loader = { workspace = true }
54+
biome_service = { workspace = true }
5455
biome_test_utils = { path = "../biome_test_utils" }
5556
criterion = { package = "codspeed-criterion-compat", version = "=3.0.3" }
5657
insta = { workspace = true, features = ["glob"] }

crates/biome_js_analyze/src/frameworks/vue/vue_component.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ use enumflags2::{BitFlags, bitflags};
2323
/// It can match any potential Vue component.
2424
pub type VueComponentQuery = Semantic<AnyPotentialVueComponent>;
2525

26-
// pub type VueComponentQuery = Semantic<AnyPotentialVueComponent>;
27-
//
2826
declare_node_union! {
2927
pub AnyPotentialVueComponent = JsExportDefaultExpressionClause
3028
| JsCallExpression

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pub mod no_unwanted_polyfillio;
3131
pub mod no_useless_backref_in_regex;
3232
pub mod no_useless_escape_in_string;
3333
pub mod no_useless_undefined;
34+
pub mod no_vue_data_object_declaration;
3435
pub mod no_vue_reserved_keys;
3536
pub mod no_vue_reserved_props;
3637
pub mod use_adjacent_getter_setter;
@@ -58,4 +59,4 @@ pub mod use_sorted_classes;
5859
pub mod use_symbol_description;
5960
pub mod use_unified_type_signature;
6061
pub mod use_unique_element_ids;
61-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_implicit_coercion :: NoImplicitCoercion , self :: no_import_cycles :: NoImportCycles , self :: no_magic_numbers :: NoMagicNumbers , self :: no_misused_promises :: NoMisusedPromises , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unassigned_variables :: NoUnassignedVariables , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_image_size :: UseImageSize , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unified_type_signature :: UseUnifiedTypeSignature , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
62+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_bitwise_operators :: NoBitwiseOperators , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_excessive_lines_per_function :: NoExcessiveLinesPerFunction , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_implicit_coercion :: NoImplicitCoercion , self :: no_import_cycles :: NoImportCycles , self :: no_magic_numbers :: NoMagicNumbers , self :: no_misused_promises :: NoMisusedPromises , self :: no_nested_component_definitions :: NoNestedComponentDefinitions , self :: no_non_null_asserted_optional_chain :: NoNonNullAssertedOptionalChain , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_process_global :: NoProcessGlobal , self :: no_qwik_use_visible_task :: NoQwikUseVisibleTask , self :: no_react_prop_assign :: NoReactPropAssign , self :: no_restricted_elements :: NoRestrictedElements , self :: no_secrets :: NoSecrets , self :: no_shadow :: NoShadow , self :: no_ts_ignore :: NoTsIgnore , self :: no_unassigned_variables :: NoUnassignedVariables , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_backref_in_regex :: NoUselessBackrefInRegex , self :: no_useless_escape_in_string :: NoUselessEscapeInString , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_adjacent_getter_setter :: UseAdjacentGetterSetter , self :: use_anchor_href :: UseAnchorHref , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_consistent_response :: UseConsistentResponse , self :: use_consistent_type_definitions :: UseConsistentTypeDefinitions , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_for_component :: UseForComponent , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_image_size :: UseImageSize , self :: use_index_of :: UseIndexOf , self :: use_iterable_callback_return :: UseIterableCallbackReturn , self :: use_json_import_attribute :: UseJsonImportAttribute , self :: use_numeric_separators :: UseNumericSeparators , self :: use_object_spread :: UseObjectSpread , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_qwik_classlist :: UseQwikClasslist , self :: use_react_function_components :: UseReactFunctionComponents , self :: use_readonly_class_properties :: UseReadonlyClassProperties , self :: use_single_js_doc_asterisk :: UseSingleJsDocAsterisk , self :: use_sorted_classes :: UseSortedClasses , self :: use_symbol_description :: UseSymbolDescription , self :: use_unified_type_signature :: UseUnifiedTypeSignature , self :: use_unique_element_ids :: UseUniqueElementIds ,] } }
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use biome_analyze::RuleSource;
2+
use biome_analyze::{
3+
FixKind, Rule, RuleDiagnostic, RuleDomain, context::RuleContext, declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_js_factory::make;
7+
use biome_js_syntax::{AnyJsExpression, AnyJsStatement, JsFileSource, JsObjectExpression, T};
8+
use biome_rowan::{AstNode, TextRange, TriviaPieceKind};
9+
use biome_rowan::{BatchMutationExt, SyntaxNodeCast};
10+
use biome_rule_options::no_vue_data_object_declaration::NoVueDataObjectDeclarationOptions;
11+
12+
use crate::JsRuleAction;
13+
use crate::frameworks::vue::vue_component::{
14+
AnyVueDataDeclarationsGroup, VueComponent, VueComponentDeclarations, VueComponentQuery,
15+
};
16+
17+
declare_lint_rule! {
18+
/// Enforce that Vue component `data` options are declared as functions.
19+
///
20+
/// In Vue 3+, defining `data` as an object is deprecated because it leads to shared mutable state across component instances.
21+
/// This rule flags usages of `data: { … }` and offers an automatic fix to convert it into a function returning that object.
22+
///
23+
/// See also:
24+
/// – Vue Migration Guide – Data Option: https://v3-migration.vuejs.org/breaking-changes/data-option.html :contentReference[oaicite:0]{index=0}
25+
/// – ESLint Plugin Vue: `no-deprecated-data-object-declaration`: https://eslint.vuejs.org/rules/no-deprecated-data-object-declaration :contentReference[oaicite:1]{index=1}
26+
///
27+
/// ## Examples
28+
///
29+
/// ### Invalid
30+
///
31+
/// ```js
32+
/// // component-local data via function
33+
/// export default {
34+
/// /* ✗ BAD */
35+
/// data: { foo: null },
36+
/// };
37+
/// ```
38+
///
39+
/// ```js
40+
/// // Composition API helper also deprecated
41+
/// defineComponent({
42+
/// /* ✗ BAD */
43+
/// data: { message: 'hi' }
44+
/// });
45+
/// ```
46+
///
47+
/// ```js
48+
/// // Vue 3 entrypoint via createApp
49+
/// createApp({
50+
/// /* ✗ BAD */
51+
/// data: { active: true }
52+
/// }).mount('#app');
53+
/// ```
54+
///
55+
/// ### Valid
56+
///
57+
/// ```js
58+
/// // component-local data via function
59+
/// export default {
60+
/// /* ✓ GOOD */
61+
/// data() {
62+
/// return { foo: null };
63+
/// }
64+
/// };
65+
/// ```
66+
///
67+
/// ```js
68+
/// // global registration with function syntax
69+
/// Vue.component('my-comp', {
70+
/// /* ✓ GOOD */
71+
/// data: function () {
72+
/// return { count: 0 };
73+
/// }
74+
/// });
75+
/// ```
76+
///
77+
/// ```js
78+
/// // Composition API and createApp entrypoints
79+
/// defineComponent({
80+
/// /* ✓ GOOD */
81+
/// data() {
82+
/// return { message: 'hi' };
83+
/// }
84+
/// });
85+
///
86+
/// createApp({
87+
/// /* ✓ GOOD */
88+
/// data: function() {
89+
/// return { active: true };
90+
/// }
91+
/// }).mount('#app');
92+
/// ```
93+
///
94+
pub NoVueDataObjectDeclaration {
95+
version: "next",
96+
name: "noVueDataObjectDeclaration",
97+
language: "vue",
98+
recommended: true,
99+
fix_kind: FixKind::Safe,
100+
domains: &[RuleDomain::Vue],
101+
sources: &[
102+
RuleSource::EslintVueJs("no-deprecated-data-object-declaration").inspired(),
103+
RuleSource::EslintVueJs("no-shared-component-data").inspired(),
104+
],
105+
}
106+
}
107+
108+
pub struct State {
109+
/// The range around the entire data declaration.
110+
data_decl_range: TextRange,
111+
112+
/// The object expression representing the value of the data declaration.
113+
object_expression: JsObjectExpression,
114+
}
115+
116+
impl Rule for NoVueDataObjectDeclaration {
117+
type Query = VueComponentQuery;
118+
type State = State;
119+
type Signals = Option<Self::State>;
120+
type Options = NoVueDataObjectDeclarationOptions;
121+
122+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
123+
let component = VueComponent::from_potential_component(
124+
ctx.query(),
125+
ctx.model(),
126+
ctx.source_type::<JsFileSource>(),
127+
)?;
128+
129+
let data_decl = component.data_declarations_group()?;
130+
131+
let data_decl_range = data_decl.range();
132+
match data_decl {
133+
AnyVueDataDeclarationsGroup::JsPropertyObjectMember(object_member) => object_member
134+
.value()
135+
.ok()
136+
.and_then(|value| value.omit_parentheses().as_js_object_expression().cloned())
137+
.map(|object_expression| State {
138+
data_decl_range,
139+
object_expression,
140+
}),
141+
AnyVueDataDeclarationsGroup::JsMethodObjectMember(_) => None,
142+
}
143+
}
144+
145+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
146+
Some(
147+
RuleDiagnostic::new(
148+
rule_category!(),
149+
state.data_decl_range,
150+
markup! {
151+
"Found an object declaration for "<Emphasis>"`data`"</Emphasis>" in this component."
152+
},
153+
)
154+
.note(markup! {
155+
"Using an object declaration for "<Emphasis>"`data`"</Emphasis>" is deprecated, and can result in different component instances sharing the same data."
156+
}),
157+
)
158+
}
159+
160+
fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
161+
let data_expr = state
162+
.object_expression
163+
.syntax()
164+
.clone()
165+
.cast::<AnyJsExpression>()?;
166+
167+
let data_function = make::js_function_expression(
168+
make::token(T![function]),
169+
make::js_parameters(
170+
make::token(T!['(']),
171+
make::js_parameter_list(None, None),
172+
make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
173+
),
174+
make::js_function_body(
175+
make::token(T!['{']).with_trailing_trivia([(TriviaPieceKind::Newline, "\n")]),
176+
make::js_directive_list(None),
177+
make::js_statement_list([AnyJsStatement::JsReturnStatement(
178+
make::js_return_statement(
179+
make::token(T![return])
180+
.with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]),
181+
)
182+
.with_argument(data_expr.clone())
183+
.build(),
184+
)]),
185+
make::token(T!['}']).with_leading_trivia([(TriviaPieceKind::Newline, "\n")]),
186+
),
187+
)
188+
.build();
189+
190+
let mut mutation = ctx.root().begin();
191+
mutation.replace_node(
192+
data_expr,
193+
AnyJsExpression::JsFunctionExpression(data_function),
194+
);
195+
196+
Some(JsRuleAction::new(
197+
ctx.metadata().action_category(ctx.category(), ctx.group()),
198+
ctx.metadata().applicability(),
199+
markup! { "Refactor the data object into a function returning the data object" }
200+
.to_owned(),
201+
mutation,
202+
))
203+
}
204+
}

crates/biome_js_analyze/src/options.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/tests/spec_tests.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use biome_js_syntax::{AnyJsRoot, EmbeddingKind, JsFileSource, JsLanguage, Module
1010
use biome_package::PackageType;
1111
use biome_plugin_loader::AnalyzerGritPlugin;
1212
use biome_rowan::{AstNode, FileSourceError};
13+
use biome_service::file_handlers::VueFileHandler;
1314
use biome_test_utils::{
1415
CheckActionType, assert_diagnostics_expectation_comment, assert_errors_are_absent,
1516
code_fix_to_string, create_analyzer_options, diagnostic_to_string,
@@ -124,11 +125,16 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
124125
} else {
125126
source_type
126127
};
128+
let input_code = if source_type.as_embedding_kind().is_vue() {
129+
VueFileHandler::input(&input_code)
130+
} else {
131+
input_code.as_str()
132+
};
127133

128134
// if source_type.
129135
analyze_and_snap(
130136
&mut snapshot,
131-
&input_code,
137+
input_code,
132138
source_type,
133139
filter,
134140
file_name,

0 commit comments

Comments
 (0)