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

Issue:
New rule: no-empty-component-block
**Please describe what the rule should do:**
The rule should avoid empty `<style scoped></style>`, script or template tags. If there are no style rules, a style tag is not necessary.

Edge case: Tags that have a src attribute (should be valid).

**What category should the rule belong to?**
- [x] Enforces code style
- [ ] 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
<template>
  <h1>Hey</h1>
</template>

<script></script>

<style scoped></style>
```

```vue
<template>
  <h1>Hey</h1>
</template>

<style scoped></style>
```

```vue
<template>
</template>

<script>
export default { 
  render (h) { return h('div','Hi') }  // Or so
}
</script>

```


Pull request description:
feat: Add no-empty-component-block rule
close #952 

This PR adds vue/no-empty-component-block rule.

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index d4ae7512..e2353b63 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -286,6 +286,7 @@ For example:
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
+| [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  |
 | [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 |  |
diff --git a/docs/rules/no-empty-component-block.md b/docs/rules/no-empty-component-block.md
new file mode 100644
index 00000000..25cc0ef1
--- /dev/null
+++ b/docs/rules/no-empty-component-block.md
@@ -0,0 +1,68 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-empty-component-block
+description: disallow the `<template>` `<script>` `<style>` block to be empty
+---
+# vue/no-empty-component-block
+> disallow the `<template>` `<script>` `<style>` block to be empty
+
+## :book: Rule Details
+
+This rule disallows the `<template>` `<script>` `<style>` block to be empty.
+
+This rule also checks block what has attribute `src`.
+See: https://vue-loader.vuejs.org/spec.html#src-imports
+
+<eslint-code-block :rules="{'vue/no-empty-component-block': ['error']}">
+
+```vue
+// ✓ GOOD
+<template>
+  <p>foo</p>
+</template>
+
+<script>
+  console.log('foo')
+</script>
+
+<style>
+  p {
+    display: inline;
+  }
+</style>
+
+<template src="./template.html"></template>
+<template src="./template.html" />
+
+<script src="./script.js"></script>
+<script src="./script.js" />
+
+<style src="./style.css"></style>
+<style src="./style.css" />
+
+
+// ✗ BAD
+<template></template>
+<template />
+<template src="" />
+
+<script></script>
+<script />
+<script src="" />
+
+<style></style>
+<style />
+<style src="" />
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-empty-component-block.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-empty-component-block.js)
diff --git a/lib/index.js b/lib/index.js
index 39510d3f..ae86686a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -69,6 +69,7 @@ module.exports = {
     'no-dupe-keys': require('./rules/no-dupe-keys'),
     'no-duplicate-attr-inheritance': require('./rules/no-duplicate-attr-inheritance'),
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
+    'no-empty-component-block': require('./rules/no-empty-component-block'),
     'no-empty-pattern': require('./rules/no-empty-pattern'),
     'no-extra-parens': require('./rules/no-extra-parens'),
     'no-irregular-whitespace': require('./rules/no-irregular-whitespace'),
diff --git a/lib/rules/no-empty-component-block.js b/lib/rules/no-empty-component-block.js
new file mode 100644
index 00000000..2df2679d
--- /dev/null
+++ b/lib/rules/no-empty-component-block.js
@@ -0,0 +1,99 @@
+/**
+ * @author tyankatsu <https://github.com/tyankatsu0105>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+/**
+ * check whether has attribute `src`
+ */
+function hasAttributeSrc(componentBlock) {
+  const hasAttribute = componentBlock.startTag.attributes.length > 0
+
+  const hasSrc =
+    componentBlock.startTag.attributes.filter(
+      (attribute) =>
+        attribute.key.name === 'src' && attribute.value.value !== ''
+    ).length > 0
+
+  return hasAttribute && hasSrc
+}
+
+/**
+ * check whether value under the component block is only whitespaces or break lines
+ */
+function isValueOnlyWhiteSpacesOrLineBreaks(componentBlock) {
+  return (
+    componentBlock.children.length === 1 &&
+    componentBlock.children[0].type === 'VText' &&
+    !componentBlock.children[0].value.trim()
+  )
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description:
+        'disallow the `<template>` `<script>` `<style>` block to be empty',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-empty-component-block.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      unexpected: '`<{{ blockName }}>` is empty. Empty block is not allowed.'
+    }
+  },
+
+  /**
+   * @param {RuleContext} context - The rule context.
+   * @returns {RuleListener} AST event handlers.
+   */
+  create(context) {
+    if (!context.parserServices.getDocumentFragment) {
+      return {}
+    }
+
+    const componentBlocks = context.parserServices.getDocumentFragment()
+      .children
+
+    return {
+      Program(node) {
+        for (const componentBlock of componentBlocks) {
+          if (
+            componentBlock.name !== 'template' &&
+            componentBlock.name !== 'script' &&
+            componentBlock.name !== 'style'
+          )
+            continue
+
+          // https://vue-loader.vuejs.org/spec.html#src-imports
+          if (hasAttributeSrc(componentBlock)) continue
+
+          if (
+            isValueOnlyWhiteSpacesOrLineBreaks(componentBlock) ||
+            componentBlock.children.length === 0
+          ) {
+            context.report({
+              node: componentBlock,
+              loc: componentBlock.loc,
+              messageId: 'unexpected',
+              data: {
+                blockName: componentBlock.name
+              }
+            })
+          }
+        }
+      }
+    }
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-empty-component-block.js b/tests/lib/rules/no-empty-component-block.js
new file mode 100644
index 00000000..92813aa2
--- /dev/null
+++ b/tests/lib/rules/no-empty-component-block.js
@@ -0,0 +1,145 @@
+/**
+ * @author tyankatsu <https://github.com/tyankatsu0105>
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-empty-component-block')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2018 }
+})
+
+tester.run('no-empty-component-block', rule, {
+  valid: [
+    `<template><p>foo</p></template>`,
+    `<template>  foobar   </template>`,
+    `<template><p>foo</p></template><script>console.log('foo')</script>`,
+    `<template><p>foo</p></template><script>console.log('foo')</script><style>p{display: inline;}</style>`,
+    `<template src="./template.html"></template>`,
+    `<template src="./template.html" />`,
+    `<template src="./template.html"></template><script src="./script.js"></script>`,
+    `<template src="./template.html" /><script src="./script.js" />`,
+    `<template src="./template.html"></template><script src="./script.js"></script><style src="./style.css"></style>`,
+    `<template src="./template.html" /><script src="./script.js" /><style src="./style.css" />`
+  ],
+  invalid: [
+    {
+      code: `<template></template>`,
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: `<template> </template>`,
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: `<template>
+</template>`,
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template src="" />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template></template><script></script>',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template /><script />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template src="" /><script src="" />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template></template><script></script><style></style>',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template /><script /><style />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    },
+    {
+      code: '<template src="" /><script src="" /><style src="" />',
+      errors: [
+        {
+          message: '`<template>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<script>` is empty. Empty block is not allowed.'
+        },
+        {
+          message: '`<style>` is empty. Empty block is not allowed.'
+        }
+      ]
+    }
+  ]
+})