Repository: vuejs/eslint-plugin-vue (JavaScript)

Issue:
Rule Proposal: Disallow style in Single File components
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
Some projects might have different CSS pipelines, and don't want to utilize `<style>` in single file components. A rule like e.g. `no-style` one could forbid it for their project.

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [ ] Enforces code style
- [x] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**
```vue
<style>
/* Example */
</style>
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->



Discussion:

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index b9d6be74b..5f848467b 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -301,6 +301,7 @@ For example:
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property |  |
 | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  |
+| [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  |
 | [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  |
 | [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option |  |
 | [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event |  |
diff --git a/docs/rules/no-restricted-block.md b/docs/rules/no-restricted-block.md
new file mode 100644
index 000000000..f84b13746
--- /dev/null
+++ b/docs/rules/no-restricted-block.md
@@ -0,0 +1,88 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-block
+description: disallow specific block
+---
+# vue/no-restricted-block
+
+> disallow specific block
+
+- :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 specify block names that you don't want to use in your application.
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a block name or pattern to be restricted:
+
+```json
+{
+  "vue/no-restricted-block": ["error", "style", "foo", "bar"]
+}
+```
+
+<eslint-code-block :rules="{'vue/no-restricted-block': ['error', 'style', 'foo', 'bar']}">
+
+```vue
+<!-- ✗ BAD -->
+<foo>
+  Custom block
+</foo>
+<bar>
+  Custom block
+</bar>
+<style>
+.foo {}
+</style>
+```
+
+</eslint-code-block>
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+  "vue/no-restricted-block": ["error",
+    {
+      "element": "style",
+      "message": "Do not use <style> block in this project."
+    },
+    {
+      "element": "foo",
+      "message": "Do not use <foo> block in this project."
+    },
+    {
+      "element": "/forbidden/",
+      "message": "Do not use blocks that include `forbidden` in their name."
+    }
+  ]
+}
+```
+
+The following properties can be specified for the object.
+
+- `element` ... Specify the block element name or pattern.
+- `message` ... Specify an optional custom message.
+
+### `{ "element": "foo"  }, { "element": "/forbidden/"  }`
+
+<eslint-code-block :rules="{'vue/no-restricted-block': ['error', { element: 'foo' }, { element: '/forbidden/' }]}">
+
+```vue
+<!-- ✗ BAD -->
+<foo>
+  ✗ BAD
+</foo>
+<forbidden-block></forbidden-block>
+<block-forbidden></block-forbidden>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-block.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-block.js)
diff --git a/lib/index.js b/lib/index.js
index 30f517169..21091a9f8 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -91,6 +91,7 @@ module.exports = {
     'no-ref-as-operand': require('./rules/no-ref-as-operand'),
     'no-reserved-component-names': require('./rules/no-reserved-component-names'),
     'no-reserved-keys': require('./rules/no-reserved-keys'),
+    'no-restricted-block': require('./rules/no-restricted-block'),
     'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
     'no-restricted-component-options': require('./rules/no-restricted-component-options'),
     'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
diff --git a/lib/rules/no-restricted-block.js b/lib/rules/no-restricted-block.js
new file mode 100644
index 000000000..b94ce0c17
--- /dev/null
+++ b/lib/rules/no-restricted-block.js
@@ -0,0 +1,126 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const regexp = require('../utils/regexp')
+/**
+ * @typedef {object} ParsedOption
+ * @property { (block: VElement) => boolean } test
+ * @property {string} [message]
+ */
+
+/**
+ * @param {string} str
+ * @returns {(str: string) => boolean}
+ */
+function buildMatcher(str) {
+  if (regexp.isRegExp(str)) {
+    const re = regexp.toRegExp(str)
+    return (s) => {
+      re.lastIndex = 0
+      return re.test(s)
+    }
+  }
+  return (s) => s === str
+}
+/**
+ * @param {any} option
+ * @returns {ParsedOption}
+ */
+function parseOption(option) {
+  if (typeof option === 'string') {
+    const matcher = buildMatcher(option)
+    return {
+      test(block) {
+        return matcher(block.rawName)
+      }
+    }
+  }
+  const parsed = parseOption(option.element)
+  parsed.message = option.message
+  return parsed
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow specific block',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-restricted-block.html'
+    },
+    fixable: null,
+    schema: {
+      type: 'array',
+      items: {
+        oneOf: [
+          { type: 'string' },
+          {
+            type: 'object',
+            properties: {
+              element: { type: 'string' },
+              message: { type: 'string', minLength: 1 }
+            },
+            required: ['element'],
+            additionalProperties: false
+          }
+        ]
+      },
+      uniqueItems: true,
+      minItems: 0
+    },
+
+    messages: {
+      // eslint-disable-next-line eslint-plugin/report-message-format
+      restrictedBlock: '{{message}}'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {ParsedOption[]} */
+    const options = context.options.map(parseOption)
+
+    const documentFragment =
+      context.parserServices.getDocumentFragment &&
+      context.parserServices.getDocumentFragment()
+
+    function getTopLevelHTMLElements() {
+      if (documentFragment) {
+        return documentFragment.children.filter(utils.isVElement)
+      }
+      return []
+    }
+
+    return {
+      /** @param {Program} node */
+      Program(node) {
+        if (utils.hasInvalidEOF(node)) {
+          return
+        }
+        for (const block of getTopLevelHTMLElements()) {
+          for (const option of options) {
+            if (option.test(block)) {
+              const message = option.message || defaultMessage(block)
+              context.report({
+                node: block.startTag,
+                messageId: 'restrictedBlock',
+                data: { message }
+              })
+              break
+            }
+          }
+        }
+      }
+    }
+
+    /**
+     * @param {VElement} block
+     */
+    function defaultMessage(block) {
+      return `Using \`<${block.rawName}>\` is not allowed.`
+    }
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-restricted-block.js b/tests/lib/rules/no-restricted-block.js
new file mode 100644
index 000000000..82b0b3b6f
--- /dev/null
+++ b/tests/lib/rules/no-restricted-block.js
@@ -0,0 +1,96 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-block')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('no-restricted-block', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<style>.foo {}</style>',
+      options: ['foo']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `<style>.foo {}</style><foo></foo>`,
+      options: ['style', 'foo'],
+      errors: [
+        {
+          message: 'Using `<style>` is not allowed.',
+          line: 1,
+          column: 1
+        },
+        {
+          message: 'Using `<foo>` is not allowed.',
+          line: 1,
+          column: 23
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<forbidden-block></forbidden-block>
+      <block-forbidden></block-forbidden>`,
+      options: ['/forbidden/'],
+      errors: [
+        'Using `<forbidden-block>` is not allowed.',
+        'Using `<block-forbidden>` is not allowed.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<style>.foo {}</style>
+      <forbidden-block></forbidden-block>
+      <block-forbidden></block-forbidden>`,
+      options: [
+        {
+          element: 'style',
+          message: 'Do not use <style> block in this project.'
+        },
+        {
+          element: '/forbidden/',
+          message: 'Do not use blocks that include `forbidden` in their name.'
+        }
+      ],
+      errors: [
+        {
+          message: 'Do not use <style> block in this project.',
+          line: 1,
+          column: 1
+        },
+        {
+          message: 'Do not use blocks that include `forbidden` in their name.',
+          line: 2,
+          column: 7
+        },
+        {
+          message: 'Do not use blocks that include `forbidden` in their name.',
+          line: 3,
+          column: 7
+        }
+      ]
+    }
+  ]
+})