1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
//! vue/require-scoped-style
//!
//! Require `scoped` attribute on `<style>` tags.
//!
//! Scoped styles prevent CSS from leaking to other components and
//! make component styles more maintainable.
//!
//! ## Examples
//!
//! ### Invalid
//! ```vue
//! <style>
//! .button { color: red; }
//! </style>
//!
//! <style lang="scss">
//! .container { padding: 20px; }
//! </style>
//! ```
//!
//! ### Valid
//! ```vue
//! <style scoped>
//! .button { color: red; }
//! </style>
//!
//! <style scoped lang="scss">
//! .container { padding: 20px; }
//! </style>
//!
//! <!-- module styles are also fine -->
//! <style module>
//! .button { color: red; }
//! </style>
//! ```
//!
//! ## Exceptions
//!
//! - Global styles in App.vue or layout components may need unscoped styles
//! - CSS reset or normalize styles
//! - Deep selectors that need to affect child components
use crate::context::LintContext;
use crate::diagnostic::{LintDiagnostic, Severity};
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_relief::ast::RootNode;
static META: RuleMeta = RuleMeta {
name: "vue/require-scoped-style",
description: "Require scoped attribute on style tags",
category: RuleCategory::Recommended,
fixable: false,
default_severity: Severity::Warning,
};
/// Require scoped style rule
#[derive(Default)]
pub struct RequireScopedStyle;
impl Rule for RequireScopedStyle {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn run_on_template<'a>(&self, ctx: &mut LintContext<'a>, _root: &RootNode<'a>) {
// This rule checks at the SFC level, not template level
// We need to check if there's an unscoped style block
// This is done during SFC parsing, so we check the source
let source = ctx.source;
// Find all <style tags
let mut pos = 0;
while let Some(style_start) = source[pos..].find("<style") {
let abs_pos = pos + style_start;
pos = abs_pos + 6;
// Find the closing >
if let Some(tag_end) = source[abs_pos..].find('>') {
let tag_content = &source[abs_pos..abs_pos + tag_end + 1];
// Check if it has scoped or module attribute
let has_scoped = tag_content.contains("scoped");
let has_module = tag_content.contains("module");
if !has_scoped && !has_module {
// Check if this is in App.vue or a layout file (common exceptions)
let filename = ctx.filename;
let is_exception = filename.ends_with("App.vue")
|| filename.contains("layout")
|| filename.contains("Layout");
if !is_exception {
ctx.report(
LintDiagnostic::warn(
META.name,
"Style block should use `scoped` or `module` attribute to prevent CSS leaking",
abs_pos as u32,
(abs_pos + tag_end + 1) as u32,
)
.with_help("Add `scoped` attribute: `<style scoped>`"),
);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
// Tests would need SFC-level testing infrastructure
// The rule checks source directly during template processing
}