|
| 1 | +use biome_analyze::{ |
| 2 | + Ast, FixKind, Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, |
| 3 | + declare_lint_rule, |
| 4 | +}; |
| 5 | +use biome_console::markup; |
| 6 | +use biome_html_syntax::VueDirective; |
| 7 | +use biome_rowan::BatchMutationExt; |
| 8 | +use biome_rowan::{AstNode, TextRange}; |
| 9 | +use biome_rule_options::use_vue_valid_v_cloak::UseVueValidVCloakOptions; |
| 10 | + |
| 11 | +declare_lint_rule! { |
| 12 | + /// Enforce valid `v-cloak` Vue directives. |
| 13 | + /// |
| 14 | + /// This rule reports `v-cloak` directives in the following cases: |
| 15 | + /// - The directive has an argument. E.g. `<div v-cloak:aaa></div>` |
| 16 | + /// - The directive has any modifiers. E.g. `<div v-cloak.bbb></div>` |
| 17 | + /// - The directive has an attribute value. E.g. `<div v-cloak="foo"></div>` |
| 18 | + /// |
| 19 | + /// ## Examples |
| 20 | + /// |
| 21 | + /// ### Invalid |
| 22 | + /// |
| 23 | + /// ```vue,expect_diagnostic |
| 24 | + /// <div v-cloak:arg></div> |
| 25 | + /// ``` |
| 26 | + /// |
| 27 | + /// ```vue,expect_diagnostic |
| 28 | + /// <div v-cloak.mod></div> |
| 29 | + /// ``` |
| 30 | + /// |
| 31 | + /// ```vue,expect_diagnostic |
| 32 | + /// <div v-cloak="value"></div> |
| 33 | + /// ``` |
| 34 | + /// |
| 35 | + /// ### Valid |
| 36 | + /// |
| 37 | + /// ```vue |
| 38 | + /// <div v-cloak></div> |
| 39 | + /// ``` |
| 40 | + /// |
| 41 | + pub UseVueValidVCloak { |
| 42 | + version: "next", |
| 43 | + name: "useVueValidVCloak", |
| 44 | + language: "html", |
| 45 | + recommended: true, |
| 46 | + domains: &[RuleDomain::Vue], |
| 47 | + sources: &[RuleSource::EslintVueJs("valid-v-cloak").same()], |
| 48 | + fix_kind: FixKind::Unsafe, |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +pub enum ViolationKind { |
| 53 | + Argument(TextRange), |
| 54 | + Modifier(TextRange), |
| 55 | + Value(TextRange), |
| 56 | +} |
| 57 | + |
| 58 | +impl Rule for UseVueValidVCloak { |
| 59 | + type Query = Ast<VueDirective>; |
| 60 | + type State = ViolationKind; |
| 61 | + type Signals = Option<Self::State>; |
| 62 | + type Options = UseVueValidVCloakOptions; |
| 63 | + |
| 64 | + fn run(ctx: &RuleContext<Self>) -> Option<Self::State> { |
| 65 | + let vue_directive = ctx.query(); |
| 66 | + if vue_directive.name_token().ok()?.text_trimmed() != "v-cloak" { |
| 67 | + return None; |
| 68 | + } |
| 69 | + |
| 70 | + if let Some(arg) = vue_directive.arg() { |
| 71 | + return Some(ViolationKind::Argument(arg.range())); |
| 72 | + } |
| 73 | + |
| 74 | + if let Some(modifier) = vue_directive.modifiers().into_iter().next() { |
| 75 | + return Some(ViolationKind::Modifier(modifier.range())); |
| 76 | + } |
| 77 | + |
| 78 | + if let Some(initializer) = vue_directive.initializer() { |
| 79 | + return Some(ViolationKind::Value(initializer.range())); |
| 80 | + } |
| 81 | + |
| 82 | + None |
| 83 | + } |
| 84 | + |
| 85 | + fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { |
| 86 | + Some(match state { |
| 87 | + ViolationKind::Argument(range) => RuleDiagnostic::new( |
| 88 | + rule_category!(), |
| 89 | + range, |
| 90 | + markup! { |
| 91 | + "The v-cloak directive must not have an argument." |
| 92 | + }, |
| 93 | + ) |
| 94 | + .note(markup! { |
| 95 | + "Use v-cloak without arguments, e.g. " <Emphasis>"v-cloak"</Emphasis> "." |
| 96 | + }), |
| 97 | + ViolationKind::Modifier(range) => RuleDiagnostic::new( |
| 98 | + rule_category!(), |
| 99 | + range, |
| 100 | + markup! { |
| 101 | + "The v-cloak directive does not support modifiers." |
| 102 | + }, |
| 103 | + ) |
| 104 | + .note(markup! { |
| 105 | + "Remove the modifier; v-cloak is a stand-alone control directive." |
| 106 | + }), |
| 107 | + ViolationKind::Value(range) => RuleDiagnostic::new( |
| 108 | + rule_category!(), |
| 109 | + range, |
| 110 | + markup! { |
| 111 | + "The v-cloak directive must not have a value." |
| 112 | + }, |
| 113 | + ) |
| 114 | + .note(markup! { |
| 115 | + "v-cloak is a boolean-like directive and should be used without a value." |
| 116 | + }), |
| 117 | + }) |
| 118 | + } |
| 119 | + |
| 120 | + fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<crate::HtmlRuleAction> { |
| 121 | + let directive = ctx.query(); |
| 122 | + let mut mutation = BatchMutationExt::begin(ctx.root()); |
| 123 | + |
| 124 | + match state { |
| 125 | + ViolationKind::Argument(_range) => { |
| 126 | + if let Some(arg) = directive.arg() { |
| 127 | + mutation.remove_node(arg); |
| 128 | + } |
| 129 | + Some(biome_analyze::RuleAction::new( |
| 130 | + ctx.metadata().action_category(ctx.category(), ctx.group()), |
| 131 | + ctx.metadata().applicability(), |
| 132 | + markup! { "Remove the argument." }.to_owned(), |
| 133 | + mutation, |
| 134 | + )) |
| 135 | + } |
| 136 | + ViolationKind::Modifier(_range) => { |
| 137 | + if let Some(first) = directive.modifiers().into_iter().next() { |
| 138 | + mutation.remove_node(first); |
| 139 | + } |
| 140 | + Some(biome_analyze::RuleAction::new( |
| 141 | + ctx.metadata().action_category(ctx.category(), ctx.group()), |
| 142 | + ctx.metadata().applicability(), |
| 143 | + markup! { "Remove the modifier." }.to_owned(), |
| 144 | + mutation, |
| 145 | + )) |
| 146 | + } |
| 147 | + ViolationKind::Value(_range) => { |
| 148 | + if let Some(initializer) = directive.initializer() { |
| 149 | + mutation.remove_node(initializer); |
| 150 | + } |
| 151 | + Some(biome_analyze::RuleAction::new( |
| 152 | + ctx.metadata().action_category(ctx.category(), ctx.group()), |
| 153 | + ctx.metadata().applicability(), |
| 154 | + markup! { "Remove the value." }.to_owned(), |
| 155 | + mutation, |
| 156 | + )) |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | +} |
0 commit comments