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

Issue:
require-prop-comment
<!--
  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:**
<!-- A clear and concise description of what the rule should do. -->
require prop should have a comment

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
[ ] Enforces code style (layout)
[ ] Warns about a potential error (problem)
[X ] Suggests an alternate way of doing something (suggestion)
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

```vue
      <template>
        <div>1</div>
      </template>
      <script setup>
      const props = defineProps({
        a: Number
      })
      </script>
```
```vue
      <template>
        <div>1</div>
      </template>
      <script>
      import { defineComponent } from '@vue/composition-api'

      export default defineComponent({
        props: {
          /**
           * a comment
           */
          a: Number,
          b: Number,
          // c
          // comment
          c: Number
        },
        setup() {}
      })
      </script>
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->



Pull request description:
Add new `vue/require-prop-comment` rule
Fixes #1993 

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f704c1a6..9ca817e3 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -260,6 +260,7 @@ For example:
 | [vue/require-emit-validator](./require-emit-validator.md) | require type definitions in emits | :bulb: | :hammer: |
 | [vue/require-expose](./require-expose.md) | require declare public properties using `expose` | :bulb: | :hammer: |
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  | :hammer: |
+| [vue/require-prop-comment](./require-prop-comment.md) | require props to have a comment |  | :hammer: |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: | :lipstick: |
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  | :hammer: |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | :hammer: |
diff --git a/docs/rules/require-prop-comment.md b/docs/rules/require-prop-comment.md
new file mode 100644
index 00000000..3b1bc29f
--- /dev/null
+++ b/docs/rules/require-prop-comment.md
@@ -0,0 +1,144 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/require-prop-comment
+description: require props to have a comment
+---
+# vue/require-prop-comment
+
+> require props to have a comment
+
+- :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 enforces that every prop has a comment that documents it.
+
+<eslint-code-block :rules="{'vue/require-prop-comment': ['error']}">
+
+```vue
+<script>
+export default defineComponent({
+  props: {
+    // ✓ GOOD
+
+    /** JSDoc comment */
+    a: Number,
+
+    // ✗ BAD
+
+    // line comment
+    b: Number,
+
+    /* block comment */
+    c: Number,
+
+    d: Number,
+  }
+})
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/require-prop-comment": ["error", {
+    "type": "JSDoc"
+  }]
+}
+```
+
+- `type` ... Type of comment. Default is `"JSDoc"`
+  - `"JSDoc"` ... Only JSDoc comment are allowed.
+  - `"line"` ... Only line comment are allowed.
+  - `"block"` ... Only block comment are allowed.
+  - `"any"` ... All comment types are allowed.
+
+### `"type": "block"`
+
+<eslint-code-block :rules="{'vue/require-prop-comment': ['error', {type: 'block'}]}">
+
+```vue
+<script setup>
+// ✓ GOOD
+const goodProps = defineProps({
+  /* block comment */
+  a: Number,
+})
+
+// ✗ BAD
+const badProps = defineProps({
+  /** JSDoc comment */
+  b: Number,
+
+  // line comment
+  c: Number,
+
+  d: Number,
+})
+</script>
+```
+
+</eslint-code-block>
+
+### `"type": "line"`
+
+<eslint-code-block :rules="{'vue/require-prop-comment': ['error', {type: 'line'}]}">
+
+```vue
+<script setup>
+// ✓ GOOD
+const goodProps = defineProps({
+  // line comment
+  a: Number,
+})
+
+// ✗ BAD
+const badProps = defineProps({
+  /** JSDoc comment */
+  b: Number,
+
+  /* block comment */
+  c: Number,
+
+  d: Number,
+})
+</script>
+```
+
+</eslint-code-block>
+
+### `"type": "any"`
+
+<eslint-code-block :rules="{'vue/require-prop-comment': ['error', {type: 'any'}]}">
+
+```vue
+<script setup>
+// ✓ GOOD
+const goodProps = defineProps({
+  /** JSDoc comment */
+  a: Number,
+
+  /* block comment */
+  b: Number,
+
+  // line comment
+  c: Number,
+})
+
+// ✗ BAD
+const badProps = defineProps({
+  d: Number,
+})
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/require-prop-comment.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/require-prop-comment.js)
diff --git a/lib/index.js b/lib/index.js
index 6f5452cc..2d14cd40 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -176,6 +176,7 @@ module.exports = {
     'require-explicit-emits': require('./rules/require-explicit-emits'),
     'require-expose': require('./rules/require-expose'),
     'require-name-property': require('./rules/require-name-property'),
+    'require-prop-comment': require('./rules/require-prop-comment'),
     'require-prop-type-constructor': require('./rules/require-prop-type-constructor'),
     'require-prop-types': require('./rules/require-prop-types'),
     'require-render-return': require('./rules/require-render-return'),
diff --git a/lib/rules/require-prop-comment.js b/lib/rules/require-prop-comment.js
new file mode 100644
index 00000000..9c8ef994
--- /dev/null
+++ b/lib/rules/require-prop-comment.js
@@ -0,0 +1,123 @@
+/**
+ * @author CZB
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'require props to have a comment',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/require-prop-comment.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          type: { enum: ['JSDoc', 'line', 'block', 'any'] }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      requireAnyComment: 'The "{{name}}" property should have a comment.',
+      requireLineComment: 'The "{{name}}" property should have a line comment.',
+      requireBlockComment:
+        'The "{{name}}" property should have a block comment.',
+      requireJSDocComment:
+        'The "{{name}}" property should have a JSDoc comment.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {{type?: "JSDoc" | "line" | "block" | "any"}|undefined} */
+    const schema = context.options[0]
+    const type = (schema && schema.type) || 'JSDoc'
+
+    const sourceCode = context.getSourceCode()
+
+    /** @param {Comment | undefined} comment */
+    const verifyBlock = (comment) =>
+      comment && comment.type === 'Block' && comment.value.charAt(0) !== '*'
+        ? undefined
+        : 'requireBlockComment'
+
+    /** @param {Comment | undefined} comment */
+    const verifyLine = (comment) =>
+      comment && comment.type === 'Line' ? undefined : 'requireLineComment'
+
+    /** @param {Comment | undefined} comment */
+    const verifyAny = (comment) => (comment ? undefined : 'requireAnyComment')
+
+    /** @param {Comment | undefined} comment */
+    const verifyJSDoc = (comment) =>
+      comment && comment.type === 'Block' && comment.value.charAt(0) === '*'
+        ? undefined
+        : 'requireJSDocComment'
+
+    /**
+     * @param {import('../utils').ComponentProp[]} props
+     */
+    function verifyProps(props) {
+      for (const prop of props) {
+        if (!prop.propName) {
+          continue
+        }
+
+        const precedingComments = sourceCode.getCommentsBefore(prop.node)
+        const lastPrecedingComment =
+          precedingComments.length > 0
+            ? precedingComments[precedingComments.length - 1]
+            : undefined
+
+        /** @type {string|undefined} */
+        let messageId
+
+        switch (type) {
+          case 'block':
+            messageId = verifyBlock(lastPrecedingComment)
+            break
+          case 'line':
+            messageId = verifyLine(lastPrecedingComment)
+            break
+          case 'any':
+            messageId = verifyAny(lastPrecedingComment)
+            break
+          default:
+            messageId = verifyJSDoc(lastPrecedingComment)
+            break
+        }
+
+        if (!messageId) {
+          continue
+        }
+
+        context.report({
+          node: prop.node,
+          messageId,
+          data: {
+            name: prop.propName
+          }
+        })
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(_node, props) {
+          verifyProps(props)
+        }
+      }),
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node) {
+          verifyProps(utils.getComponentPropsFromOptions(node))
+        }
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/require-prop-comment.js b/tests/lib/rules/require-prop-comment.js
new file mode 100644
index 00000000..f72d6905
--- /dev/null
+++ b/tests/lib/rules/require-prop-comment.js
@@ -0,0 +1,269 @@
+/**
+ * @author CZB
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/require-prop-comment')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('require-prop-comment', rule, {
+  valid: [
+    {
+      code: `
+      <script setup>
+      export default defineComponent({
+        props: {
+          /** JSDoc comment */
+          a: Number,
+        }
+      })
+      </script>
+      `
+    },
+    {
+      code: `
+      <script setup>
+      const goodProps = defineProps({
+        /* block comment */
+        a: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'block' }]
+    },
+    {
+      code: `
+      <script setup>
+      const goodProps = defineProps({
+        // line comment
+        a: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'line' }]
+    },
+    {
+      code: `
+      <script setup>
+      const goodProps = defineProps({
+        /** JSDoc comment */
+        a: Number,
+      
+        /* block comment */
+        b: Number,
+      
+        // line comment
+        c: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'any' }]
+    },
+    {
+      code: `
+      <script lang="ts">
+      export default defineComponent({
+        props: {
+          /** JSDoc comment */
+          a: Number
+        }
+      })
+      </script>
+      `
+    },
+    {
+      code: `
+      <script setup lang="ts">
+      type PropType = {
+        /** JSDoc comment */
+        a: number
+      }
+      const props = defineProps<PropType>()
+      </script>
+      `,
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      export default defineComponent({
+        props: {
+          // line comment
+          b: Number,
+      
+          /* block comment */
+          c: Number,
+      
+          d: Number,
+        }
+      })
+      `,
+      errors: [
+        {
+          line: 5,
+          column: 11,
+          message: `The "b" property should have a JSDoc comment.`
+        },
+        {
+          line: 8,
+          column: 11,
+          message: `The "c" property should have a JSDoc comment.`
+        },
+        {
+          line: 10,
+          column: 11,
+          message: `The "d" property should have a JSDoc comment.`
+        }
+      ]
+    },
+    {
+      code: `
+      <script setup>
+      const badProps = defineProps({
+        /** JSDoc comment */
+        b: Number,
+      
+        // line comment
+        c: Number,
+      
+        d: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'block' }],
+      errors: [
+        {
+          line: 5,
+          column: 9,
+          message: 'The "b" property should have a block comment.'
+        },
+        {
+          line: 8,
+          column: 9,
+          message: 'The "c" property should have a block comment.'
+        },
+        {
+          line: 10,
+          column: 9,
+          message: 'The "d" property should have a block comment.'
+        }
+      ]
+    },
+    {
+      code: `
+      <script setup>
+      const badProps = defineProps({
+        /** JSDoc comment */
+        b: Number,
+      
+        /* block comment */
+        c: Number,
+      
+        d: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'line' }],
+      errors: [
+        {
+          line: 5,
+          column: 9,
+          message: 'The "b" property should have a line comment.'
+        },
+        {
+          line: 8,
+          column: 9,
+          message: 'The "c" property should have a line comment.'
+        },
+        {
+          line: 10,
+          column: 9,
+          message: 'The "d" property should have a line comment.'
+        }
+      ]
+    },
+    {
+      code: `
+      <script setup>
+      const badProps = defineProps({
+        d: Number,
+      })
+      </script>
+      `,
+      options: [{ type: 'any' }],
+      errors: [
+        {
+          line: 4,
+          column: 9,
+          message: `The "d" property should have a comment.`
+        }
+      ]
+    },
+    {
+      code: `
+      <script lang="ts">
+      export default defineComponent({
+        props: {
+          a: Number
+        }
+      })
+      </script>
+      `,
+      errors: [
+        {
+          line: 5,
+          column: 11,
+          message: 'The "a" property should have a JSDoc comment.'
+        }
+      ]
+    },
+    {
+      code: `
+      new Vue({
+        props: {
+          a: Number
+        }
+      })
+      `,
+      errors: [
+        {
+          line: 4,
+          column: 11,
+          message: 'The "a" property should have a JSDoc comment.'
+        }
+      ]
+    },
+    {
+      code: `
+      <script setup lang="ts">
+      type PropType = {
+        a: number
+      }
+      const props = defineProps<PropType>()
+      </script>
+      `,
+      errors: [
+        {
+          line: 4,
+          column: 9,
+          message: 'The "a" property should have a JSDoc comment.'
+        }
+      ],
+      parserOptions: {
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    }
+  ]
+})