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
83 changes: 83 additions & 0 deletions docs/rules/enforce-style-attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/enforce-style-attribute
description: enforce either the `scoped` or `module` attribute in SFC top level style tags
---

# vue/enforce-style-attribute

> enfore either the `scoped` or `module` attribute 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>

## :wrench: Options

```json
{
"vue/attribute-hyphenation": ["error", "either" | "scoped" | "module"]
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
}
```

## :book: Rule Details

This rule warns you about top level style tags that are missing either the `scoped` or `module` attribute.

- `"either"` (default) ... Warn if a style tag doesn't have neither `scoped` nor `module` attributes.
- `"scoped"` ... Warn if a style tag doesn't have the `scoped` attribute.
- `"module"` ... Warn if a style tag doesn't have the `module` attribute.

### `"either"`

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

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

<!-- ✓ GOOD -->
<style module></style>

<!-- ✗ BAD -->
<style></style>
```

</eslint-code-block>

### `"scoped"`

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

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

<!-- ✗ BAD -->
<style></style>
<style module></style>
```

</eslint-code-block>

### `"module"`

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

```vue
<!-- ✓ GOOD -->
<style module></style>

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

</eslint-code-block>

## :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)
152 changes: 152 additions & 0 deletions lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* @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].allows) {
return context.options[0].allows
}
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: 'code',
mussinbenarbia marked this conversation as resolved.
Show resolved Hide resolved
schema: [
{
type: 'object',
properties: {
allows: {
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
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) {
if (!context.parserServices.getDocumentFragment) {
return {}
}
const documentFragment = context.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
}
}
}
}
}
}
10 changes: 6 additions & 4 deletions tests/integrations/eslint-plugin-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ describe('Integration with eslint-plugin-import', () => {
if (
!semver.satisfies(
process.version,
require(path.join(
__dirname,
'eslint-plugin-import/node_modules/eslint/package.json'
)).engines.node
require(
path.join(
__dirname,
'eslint-plugin-import/node_modules/eslint/package.json'
)
).engines.node
)
) {
return
Expand Down
Loading