|
| 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 | +} |
0 commit comments