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
85 changes: 85 additions & 0 deletions docs/rules/enforce-style-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
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 selectively allow attributes on your top level style tags and warns when using an attribute that is not allowed.

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

### `"no-attributes"`

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

```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", "no-attributes"] }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. I think the name no-attributes is misleading. lang="scss" has attributes. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah... that could be confusing 🤔 Maybe using plain like in the other plugin is the best choice here. If you agree I'll rename it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think plain is better 👍

]
}
```

- `"allow"` (`["scoped" | "module" | "no-attributes"]`) Array of attributes to allow on a top level style tag. 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 hasNoAttributes(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: ['no-attributes', 'scoped', 'module']
}
}
},
additionalProperties: false
}
],
messages: {
notAllowedScoped:
'The scoped attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
notAllowedModule:
'The module attribute is not allowed. Allowed: {{ allowedAttrsString }}.',
notAllowedNoAttributes:
'A <style> tag without attributes is 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 allowsNoAttributes = allowedAttrs.includes('no-attributes')
const allowsScoped = allowedAttrs.includes('scoped')
const allowsModule = allowedAttrs.includes('module')
const allowedAttrsString = [...allowedAttrs].sort().join(', ')

return {
Program() {
for (const styleTag of topLevelStyleTags) {
if (!allowsNoAttributes && hasNoAttributes(styleTag)) {
context.report({
node: styleTag,
messageId: 'notAllowedNoAttributes',
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