Skip to content

Commit

Permalink
Add new vue/enforce-style-attribute rule (#2110)
Browse files Browse the repository at this point in the history
Co-authored-by: Flo Edelmann <[email protected]>
Co-authored-by: Mussin Benarbia <[email protected]>
  • Loading branch information
3 people authored Jan 9, 2024
1 parent e2f8b70 commit c232e26
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 0 deletions.
86 changes: 86 additions & 0 deletions docs/rules/enforce-style-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/enforce-style-attribute
description: enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
---

# vue/enforce-style-attribute

> enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>

## :book: Rule Details

This rule allows you to explicitly allow the use of the `scoped` and `module` attributes on your top level style tags.

### `"scoped"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['scoped'] }]}">

```vue
<!-- ✓ GOOD -->
<style scoped></style>
<style lang="scss" src="../path/to/style.scss" scoped></style>
<!-- ✗ BAD -->
<style module></style>
<!-- ✗ BAD -->
<style></style>
```

</eslint-code-block>

### `"module"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['module'] }]}">

```vue
<!-- ✓ GOOD -->
<style module></style>
<!-- ✗ BAD -->
<style scoped></style>
<!-- ✗ BAD -->
<style></style>
```

</eslint-code-block>

### `"plain"`

<eslint-code-block :rules="{'vue/enforce-style-attribute': ['error', { allow: ['plain']}]}">

```vue
<!-- ✓ GOOD -->
<style></style>
<!-- ✗ BAD -->
<style scoped></style>
<!-- ✗ BAD -->
<style module></style>
```

</eslint-code-block>

## :wrench: Options

```json
{
"vue/enforce-style-attribute": [
"error",
{ "allow": ["scoped", "module", "plain"] }
]
}
```

- `"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"]`

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/enforce-style-attribute.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/enforce-style-attribute.js)
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ For example:
| [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` | | :hammer: |
| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: | :lipstick: |
| [vue/define-props-declaration](./define-props-declaration.md) | enforce declaration style of `defineProps` | | :hammer: |
| [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: |
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | :hammer: |
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | :lipstick: |
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | :lipstick: |
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ module.exports = {
'define-props-declaration': require('./rules/define-props-declaration'),
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
'enforce-style-attribute': require('./rules/enforce-style-attribute'),
eqeqeq: require('./rules/eqeqeq'),
'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
'func-call-spacing': require('./rules/func-call-spacing'),
Expand Down
153 changes: 153 additions & 0 deletions lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* @author Mussin Benarbia
* See LICENSE file in root directory for full license.
*/
'use strict'

const { isVElement } = require('../utils')

/**
* check whether a tag has the `scoped` attribute
* @param {VElement} componentBlock
*/
function isScoped(componentBlock) {
return componentBlock.startTag.attributes.some(
(attribute) => !attribute.directive && attribute.key.name === 'scoped'
)
}

/**
* check whether a tag has the `module` attribute
* @param {VElement} componentBlock
*/
function isModule(componentBlock) {
return componentBlock.startTag.attributes.some(
(attribute) => !attribute.directive && attribute.key.name === 'module'
)
}

/**
* check if a tag doesn't have either the `scoped` nor `module` attribute
* @param {VElement} componentBlock
*/
function isPlain(componentBlock) {
return !isScoped(componentBlock) && !isModule(componentBlock)
}

function getUserDefinedAllowedAttrs(context) {
if (context.options[0] && context.options[0].allow) {
return context.options[0].allow
}
return []
}

const defaultAllowedAttrs = ['scoped']

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/enforce-style-attribute.html'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
allow: {
type: 'array',
minItems: 1,
uniqueItems: true,
items: {
type: 'string',
enum: ['plain', 'scoped', 'module']
}
}
},
additionalProperties: false
}
],
messages: {
notAllowedScoped:
'The scoped attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
notAllowedModule:
'The module attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
notAllowedPlain:
'Plain <style> tags are not allowed. Allowed: {{ allowedAttrsString }}.'
}
},

/** @param {RuleContext} context */
create(context) {
const sourceCode = context.getSourceCode()
if (!sourceCode.parserServices.getDocumentFragment) {
return {}
}
const documentFragment = sourceCode.parserServices.getDocumentFragment()
if (!documentFragment) {
return {}
}

const topLevelElements = documentFragment.children.filter(isVElement)
const topLevelStyleTags = topLevelElements.filter(
(element) => element.rawName === 'style'
)

if (topLevelStyleTags.length === 0) {
return {}
}

const userDefinedAllowedAttrs = getUserDefinedAllowedAttrs(context)
const allowedAttrs =
userDefinedAllowedAttrs.length > 0
? userDefinedAllowedAttrs
: defaultAllowedAttrs

const allowsPlain = allowedAttrs.includes('plain')
const allowsScoped = allowedAttrs.includes('scoped')
const allowsModule = allowedAttrs.includes('module')
const allowedAttrsString = [...allowedAttrs].sort().join(', ')

return {
Program() {
for (const styleTag of topLevelStyleTags) {
if (!allowsPlain && isPlain(styleTag)) {
context.report({
node: styleTag,
messageId: 'notAllowedPlain',
data: {
allowedAttrsString
}
})
return
}

if (!allowsScoped && isScoped(styleTag)) {
context.report({
node: styleTag,
messageId: 'notAllowedScoped',
data: {
allowedAttrsString
}
})
return
}

if (!allowsModule && isModule(styleTag)) {
context.report({
node: styleTag,
messageId: 'notAllowedModule',
data: {
allowedAttrsString
}
})
return
}
}
}
}
}
}
Loading

0 comments on commit c232e26

Please sign in to comment.