diff --git a/docs/rules/enforce-style-attribute.md b/docs/rules/enforce-style-attribute.md
new file mode 100644
index 000000000..6dc042353
--- /dev/null
+++ b/docs/rules/enforce-style-attribute.md
@@ -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: **_This rule has not been released yet._**
+
+## :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"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"module"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+### `"plain"`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :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)
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 71a05d42e..912d5ae46 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -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: |
diff --git a/lib/index.js b/lib/index.js
index 3497a7b4f..3eb5208ce 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/enforce-style-attribute.js b/lib/rules/enforce-style-attribute.js
new file mode 100644
index 000000000..b4b39a73f
--- /dev/null
+++ b/lib/rules/enforce-style-attribute.js
@@ -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 '
+ },
+ // With scoped option
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['scoped'] }]
+ },
+ // With module option
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['module'] }]
+ },
+ // With plain option
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['plain'] }]
+ },
+ // With all options
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['scoped', 'module', 'plain'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['scoped', 'module', 'plain'] }]
+ },
+ {
+ filename: 'test.vue',
+ code: '',
+ options: [{ allow: ['scoped', 'module', 'plain'] }]
+ }
+ ],
+ invalid: [
+ // With default (scoped)
+ {
+ code: ``,
+ errors: [
+ {
+ message: 'Plain `,
+ errors: [
+ {
+ message: 'The module attribute is not allowed. Allowed: scoped.'
+ }
+ ]
+ },
+ // With scoped option
+ {
+ code: ``,
+ options: [{ allow: ['scoped'] }],
+ errors: [
+ {
+ message: 'Plain `,
+ options: [{ allow: ['scoped'] }],
+ errors: [
+ {
+ message: 'The module attribute is not allowed. Allowed: scoped.'
+ }
+ ]
+ },
+ // With module option
+ {
+ code: ``,
+ options: [{ allow: ['module'] }],
+ errors: [
+ {
+ message: 'Plain `,
+ options: [{ allow: ['module'] }],
+ errors: [
+ {
+ message: 'The scoped attribute is not allowed. Allowed: module.'
+ }
+ ]
+ },
+ // With different combinations of two options
+ {
+ code: ``,
+ options: [{ allow: ['module', 'scoped'] }],
+ errors: [
+ {
+ message:
+ 'Plain `,
+ options: [{ allow: ['scoped', 'plain'] }],
+ errors: [
+ {
+ message:
+ 'The module attribute is not allowed. Allowed: plain, scoped.'
+ }
+ ]
+ },
+ {
+ code: ``,
+ options: [{ allow: ['module', 'plain'] }],
+ errors: [
+ {
+ message:
+ 'The scoped attribute is not allowed. Allowed: module, plain.'
+ }
+ ]
+ }
+ ]
+})