Skip to content

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

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

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

## :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)
102 changes: 102 additions & 0 deletions lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @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'
)
}

module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'enforce either the `scoped` or `module` attribute in SFC top level style tags',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/enforce-style-attribute.html'
},
fixable: 'code',
schema: [{ enum: ['scoped', 'module', 'either'] }],
messages: {
needsScoped: 'The <style> tag should have the scoped attribute.',
needsModule: 'The <style> tag should have the module attribute.',
needsEither:
'The <style> tag should have either the scoped or module attribute.'
}
},

/** @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 mode = context.options[0] || 'either'
const needsScoped = mode === 'scoped'
const needsModule = mode === 'module'
const needsEither = mode === 'either'

return {
Program() {
for (const styleTag of topLevelStyleTags) {
if (needsScoped && !isScoped(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsScoped'
})
return
}

if (needsModule && !isModule(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsModule'
})
return
}

if (needsEither && !isScoped(styleTag) && !isModule(styleTag)) {
context.report({
node: styleTag,
messageId: 'needsEither'
})
return
}
}
}
}
}
}
101 changes: 101 additions & 0 deletions tests/lib/rules/enforce-style-attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @author Mussin Benarbia
* See LICENSE file in root directory for full license.
*/
'use strict'

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/enforce-style-attribute')

const tester = new RuleTester({
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module'
}
})

tester.run('enforce-style-attribute', rule, {
valid: [
// With default options
{
filename: 'test.vue',
code: '<template></template><script></script><style scoped></style>'
},
{
filename: 'test.vue',
code: '<template></template><script></script><style module></style>'
},
{
filename: 'test.vue',
code: '<template></template><script></script><style lang=scss" "src="../path/to/style.scss" scoped></style>'
},
// With scoped option
{
filename: 'test.vue',
code: '<template></template><script></script><style scoped></style>',
options: ['scoped']
},
{
filename: 'test.vue',
code: '<template></template><script></script><style lang=scss" "src="../path/to/style.scss" scoped></style>',
options: ['scoped']
},
// With module option
{
filename: 'test.vue',
code: '<template></template><script></script><style module></style>',
options: ['module']
}
],
invalid: [
// With default options
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message:
'The <style> tag should have either the scoped or module attribute.'
}
]
},
// With scoped option
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message: 'The <style> tag should have the scoped attribute.'
}
],
options: ['scoped']
},
{
code: `<template></template><script></script><style module></style>`,
errors: [
{
message: 'The <style> tag should have the scoped attribute.'
}
],
options: ['scoped']
},
// With module option
{
code: `<template></template><script></script><style></style>`,
errors: [
{
message: 'The <style> tag should have the module attribute.'
}
],
options: ['module']
},
{
code: `<template></template><script></script><style scoped></style>`,
errors: [
{
message: 'The <style> tag should have the module attribute.'
}
],
options: ['module']
}
]
})