Skip to content

Commit c232e26

Browse files
mussinbenarbiaFloEdelmannMussin Benarbia
authored
Add new vue/enforce-style-attribute rule (#2110)
Co-authored-by: Flo Edelmann <[email protected]> Co-authored-by: Mussin Benarbia <[email protected]>
1 parent e2f8b70 commit c232e26

File tree

5 files changed

+389
-0
lines changed

5 files changed

+389
-0
lines changed

docs/rules/enforce-style-attribute.md

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/enforce-style-attribute
5+
description: enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
6+
---
7+
8+
# vue/enforce-style-attribute
9+
10+
> enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
14+
## :book: Rule Details
15+
16+
This rule allows you to explicitly allow the use of the `scoped` and `module` attributes on your top level style tags.
17+
18+
### `"scoped"`
19+
20+
<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['scoped'] }]}">
21+
22+
```vue
23+
<!-- ✓ GOOD -->
24+
<style scoped></style>
25+
<style lang="scss" src="../path/to/style.scss" scoped></style>
26+
27+
<!-- ✗ BAD -->
28+
<style module></style>
29+
30+
<!-- ✗ BAD -->
31+
<style></style>
32+
```
33+
34+
</eslint-code-block>
35+
36+
### `"module"`
37+
38+
<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['module'] }]}">
39+
40+
```vue
41+
<!-- ✓ GOOD -->
42+
<style module></style>
43+
44+
<!-- ✗ BAD -->
45+
<style scoped></style>
46+
47+
<!-- ✗ BAD -->
48+
<style></style>
49+
```
50+
51+
</eslint-code-block>
52+
53+
### `"plain"`
54+
55+
<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['plain']}]}">
56+
57+
```vue
58+
<!-- ✓ GOOD -->
59+
<style></style>
60+
61+
<!-- ✗ BAD -->
62+
<style scoped></style>
63+
64+
<!-- ✗ BAD -->
65+
<style module></style>
66+
```
67+
68+
</eslint-code-block>
69+
70+
## :wrench: Options
71+
72+
```json
73+
{
74+
"vue/enforce-style-attribute": [
75+
"error",
76+
{ "allow": ["scoped", "module", "plain"] }
77+
]
78+
}
79+
```
80+
81+
- `"allow"` (`["scoped" | "module" | "plain"]`) Array of attributes to allow on a top level style tag. The option `plain` is used to allow style tags that have neither the `scoped` nor `module` attributes. Default: `["scoped"]`
82+
83+
## :mag: Implementation
84+
85+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/enforce-style-attribute.js)
86+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/enforce-style-attribute.js)

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ For example:
215215
| [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` | | :hammer: |
216216
| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: | :lipstick: |
217217
| [vue/define-props-declaration](./define-props-declaration.md) | enforce declaration style of `defineProps` | | :hammer: |
218+
| [vue/enforce-style-attribute](./enforce-style-attribute.md) | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags | | :hammer: |
218219
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | :hammer: |
219220
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | :lipstick: |
220221
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | :lipstick: |

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ module.exports = {
3535
'define-props-declaration': require('./rules/define-props-declaration'),
3636
'dot-location': require('./rules/dot-location'),
3737
'dot-notation': require('./rules/dot-notation'),
38+
'enforce-style-attribute': require('./rules/enforce-style-attribute'),
3839
eqeqeq: require('./rules/eqeqeq'),
3940
'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
4041
'func-call-spacing': require('./rules/func-call-spacing'),

lib/rules/enforce-style-attribute.js

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @author Mussin Benarbia
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { isVElement } = require('../utils')
8+
9+
/**
10+
* check whether a tag has the `scoped` attribute
11+
* @param {VElement} componentBlock
12+
*/
13+
function isScoped(componentBlock) {
14+
return componentBlock.startTag.attributes.some(
15+
(attribute) => !attribute.directive && attribute.key.name === 'scoped'
16+
)
17+
}
18+
19+
/**
20+
* check whether a tag has the `module` attribute
21+
* @param {VElement} componentBlock
22+
*/
23+
function isModule(componentBlock) {
24+
return componentBlock.startTag.attributes.some(
25+
(attribute) => !attribute.directive && attribute.key.name === 'module'
26+
)
27+
}
28+
29+
/**
30+
* check if a tag doesn't have either the `scoped` nor `module` attribute
31+
* @param {VElement} componentBlock
32+
*/
33+
function isPlain(componentBlock) {
34+
return !isScoped(componentBlock) && !isModule(componentBlock)
35+
}
36+
37+
function getUserDefinedAllowedAttrs(context) {
38+
if (context.options[0] && context.options[0].allow) {
39+
return context.options[0].allow
40+
}
41+
return []
42+
}
43+
44+
const defaultAllowedAttrs = ['scoped']
45+
46+
module.exports = {
47+
meta: {
48+
type: 'suggestion',
49+
docs: {
50+
description:
51+
'enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags',
52+
categories: undefined,
53+
url: 'https://eslint.vuejs.org/rules/enforce-style-attribute.html'
54+
},
55+
fixable: null,
56+
schema: [
57+
{
58+
type: 'object',
59+
properties: {
60+
allow: {
61+
type: 'array',
62+
minItems: 1,
63+
uniqueItems: true,
64+
items: {
65+
type: 'string',
66+
enum: ['plain', 'scoped', 'module']
67+
}
68+
}
69+
},
70+
additionalProperties: false
71+
}
72+
],
73+
messages: {
74+
notAllowedScoped:
75+
'The scoped attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
76+
notAllowedModule:
77+
'The module attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
78+
notAllowedPlain:
79+
'Plain <style> tags are not allowed. Allowed: {{ allowedAttrsString }}.'
80+
}
81+
},
82+
83+
/** @param {RuleContext} context */
84+
create(context) {
85+
const sourceCode = context.getSourceCode()
86+
if (!sourceCode.parserServices.getDocumentFragment) {
87+
return {}
88+
}
89+
const documentFragment = sourceCode.parserServices.getDocumentFragment()
90+
if (!documentFragment) {
91+
return {}
92+
}
93+
94+
const topLevelElements = documentFragment.children.filter(isVElement)
95+
const topLevelStyleTags = topLevelElements.filter(
96+
(element) => element.rawName === 'style'
97+
)
98+
99+
if (topLevelStyleTags.length === 0) {
100+
return {}
101+
}
102+
103+
const userDefinedAllowedAttrs = getUserDefinedAllowedAttrs(context)
104+
const allowedAttrs =
105+
userDefinedAllowedAttrs.length > 0
106+
? userDefinedAllowedAttrs
107+
: defaultAllowedAttrs
108+
109+
const allowsPlain = allowedAttrs.includes('plain')
110+
const allowsScoped = allowedAttrs.includes('scoped')
111+
const allowsModule = allowedAttrs.includes('module')
112+
const allowedAttrsString = [...allowedAttrs].sort().join(', ')
113+
114+
return {
115+
Program() {
116+
for (const styleTag of topLevelStyleTags) {
117+
if (!allowsPlain && isPlain(styleTag)) {
118+
context.report({
119+
node: styleTag,
120+
messageId: 'notAllowedPlain',
121+
data: {
122+
allowedAttrsString
123+
}
124+
})
125+
return
126+
}
127+
128+
if (!allowsScoped && isScoped(styleTag)) {
129+
context.report({
130+
node: styleTag,
131+
messageId: 'notAllowedScoped',
132+
data: {
133+
allowedAttrsString
134+
}
135+
})
136+
return
137+
}
138+
139+
if (!allowsModule && isModule(styleTag)) {
140+
context.report({
141+
node: styleTag,
142+
messageId: 'notAllowedModule',
143+
data: {
144+
allowedAttrsString
145+
}
146+
})
147+
return
148+
}
149+
}
150+
}
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)