Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new vue/enforce-style-attribute rule #2110

Merged
merged 13 commits into from
Jan 9, 2024
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 @@
'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 Expand Up @@ -261,7 +262,7 @@
'.vue': require('./processor')
},
environments: {
// TODO Remove in the next major version

Check warning on line 265 in lib/index.js

View workflow job for this annotation

GitHub Actions / Lint

Unexpected 'todo' comment: 'TODO Remove in the next major version'
/** @deprecated */
'setup-compiler-macros': {
globals: {
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
Loading