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

Issue:
False positive with `vue/require-default-prop`
**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.

**Tell us about your environment**

- **ESLint version:** 7.32.0
- **eslint-plugin-vue version:** 7.15.0
- **Node version:** 16.4.1
- **Operating System:** MacOS Big Sur 11.5.1

**Please show your full configuration:**
```json5
module.exports = {
  root: true,

  env: {
    node: true,
    'jest/globals': true,
  },

  plugins: ['jest'],

  extends: [
    // add more generic rulesets here, such as:
    'airbnb-base',
    'eslint:recommended',
    'plugin:vue/vue3-recommended',
    'plugin:jest/recommended',
    'plugin:jest/style',
  ],

  parserOptions: {
    parser: '@typescript-eslint/parser',
  },

  globals: {
    google: 'readonly',
    RecursivePartial: 'readonly',
  },

  ignorePatterns: ['*.json'],

  rules: {
    // ESLINT
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
    quotes: ['error', 'single'],
    'prefer-destructuring': ['error', {
      object: true,
      array: false,
    }],
    'import/no-unresolved': 'off',
    'import/extensions': 'off',
    'import/no-extraneous-dependencies': 'off',
    'import/prefer-default-export': 'off',

    'operator-linebreak': ['error', 'after'],
    'comma-spacing': ['error'],
    'comma-dangle': ['error', 'always-multiline'],
    'arrow-spacing': ['error'],
    'brace-style': ['error'],
    'comma-style': ['error', 'last'],
    'dot-location': ['error', 'property'],
    'dot-notation': ['error'],
    eqeqeq: ['error'],
    'func-call-spacing': ['error', 'never'],
    'key-spacing': ['error'],
    'keyword-spacing': ['error'],
    'no-extra-parens': ['error'],
    'no-useless-concat': ['error'],
    'object-curly-spacing': ['error', 'always'],
    'prefer-template': ['error'],
    'space-in-parens': ['error', 'never'],
    'space-infix-ops': ['error'],
    'space-unary-ops': [
      2, {
        words: true,
        nonwords: false,
      }],
    'template-curly-spacing': ['error'],
    'no-use-before-define': ['error', { functions: false }],
    'max-len': 'off',

    // Vue Uncategorized
    'vue/html-comment-content-newline': ['error', {
      singleline: 'never',
      multiline: 'always',
    }],
    'vue/custom-event-name-casing': ['error', 'camelCase'],
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/html-comment-content-spacing': ['error', 'always'],
    'vue/html-comment-indent': ['error', 'tab'],
    'vue/match-component-file-name': ['error', { extensions: ['vue'], shouldMatchCase: true }],
    'vue/new-line-between-multi-line-property': ['error'],
    'vue/no-duplicate-attr-inheritance': ['error'],
    'vue/no-empty-component-block': ['error'],
    'vue/no-invalid-model-keys': ['error'],
    'vue/no-multiple-objects-in-class': ['error'],
    'vue/no-potential-component-option-typo': ['error', { presets: ['all'] }],
    'vue/no-reserved-component-names': ['error', {
      disallowVueBuiltInComponents: false,
      disallowVue3BuiltInComponents: false,
    }],
    'vue/no-static-inline-styles': ['error'],
    'vue/no-unused-properties': ['error'],
    'vue/no-unused-refs': ['error'],
    'vue/no-useless-mustaches': ['error'],
    'vue/no-useless-v-bind': ['error'],
    'vue/padding-line-between-blocks': ['error', 'always'],
    'vue/require-direct-export': ['error'],
    'vue/script-indent': ['error'],
    'vue/v-for-delimiter-style': ['error', 'in'],
    'vue/v-on-event-hyphenation': ['error', 'never'],
    'vue/v-on-function-call': ['error', 'always'],
    'vue/script-setup-uses-vars': ['off'],

    // Vue Extension Rules
    'vue/comma-spacing': ['error'],
    'vue/comma-style': ['error', 'last'],
    'vue/dot-location': ['error', 'property'],
    'vue/dot-notation': ['error'],
    'vue/eqeqeq': ['error'],
    'vue/func-call-spacing': ['error', 'never'],
    'vue/key-spacing': ['error'],
    'vue/keyword-spacing': ['error'],
    'vue/no-constant-condition': ['error'],
    'vue/no-empty-pattern': ['error'],
    'vue/no-extra-parens': ['error'],
    'vue/no-irregular-whitespace': ['error'],
    // 'vue/no-restricted-syntax': ['error', {
    //   selector: 'VElement > VExpressionContainer CallExpression',
    //   message: 'Can\'t call expressions inside mustache interpolation',
    // }],
    'vue/no-sparse-arrays': ['error'],
    'vue/no-useless-concat': ['error'],
    'vue/object-curly-spacing': ['error', 'always'],
    'vue/prefer-template': ['error'],
    'vue/space-in-parens': ['error', 'never'],
    'vue/space-infix-ops': ['error'],
    'vue/template-curly-spacing': ['error'],
    'vue/v-slot-style': ['error', { default: 'v-slot', atComponent: 'v-slot', named: 'longform' }],

    // SPECIALS

    // Use this to detect bare strings to replace with i18n https://eslint.vuejs.org/rules/no-bare-strings-in-template.html
    // 'vue/no-bare-strings-in-template': ['error'],
  },

  overrides: [{
    files: ['*.vue'],
    rules: {
      'no-unused-vars': 'off',
    },
  }, {
    files: ['*.d.ts'],
    rules: {
      'no-unused-vars': 'off',
    },
  }],
};

```

**What did you do?**	
```vue
<template>
  <div>
    {{ required }}
    {{ optional }}
  </div>
</template>

<script setup lang="ts">
import { defineProps, withDefaults } from 'vue';

interface Props {
  required: boolean;
  optional?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  optional: false,
});
</script>

```

**What did you expect to happen?**
The `required` prop should not list any warning because is a required property


**What actually happened?**
```
  12:3  warning  Prop 'required' requires default value to be set  vue/require-default-prop
```

**Repository to reproduce this issue**
I'll try to make one the next week, I don't have time this :( 



Discussion:

Patch:
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index d35c2f8ea..e3a8a4076 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -175,6 +175,12 @@ module.exports = {
           withDefaults &&
           withDefaultsExpressions
         ) {
+          if (prop.required) {
+            continue
+          }
+          if (prop.types.length === 1 && prop.types[0] === 'Boolean') {
+            continue
+          }
           if (!withDefaultsExpressions[prop.propName]) {
             context.report({
               node: prop.node,


Test patch:
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index d93c5e5da..e129ae84d 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -235,7 +235,7 @@ ruleTester.run('require-default-prop', rule, {
       code: `
       <script setup lang="ts">
       interface Props {
-        foo: number
+        foo?: number
       }
       defineProps<Props>()
       </script>
@@ -251,7 +251,7 @@ ruleTester.run('require-default-prop', rule, {
       code: `
       <script setup lang="ts">
       interface Props {
-        foo: number
+        foo?: number
       }
       withDefaults(defineProps<Props>(), {foo:42})
       </script>
@@ -267,7 +267,7 @@ ruleTester.run('require-default-prop', rule, {
       code: `
       <script setup lang="ts">
       interface Props {
-        foo: number
+        foo?: number
       }
       defineProps<Props>({
         foo:{
@@ -281,6 +281,53 @@ ruleTester.run('require-default-prop', rule, {
         ...parserOptions,
         parser: require.resolve('@typescript-eslint/parser')
       }
+    },
+    {
+      // https://github.com/vuejs/eslint-plugin-vue/issues/1591
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          {{ required }}
+          {{ optional }}
+        </div>
+      </template>
+
+      <script setup lang="ts">
+      import { defineProps, withDefaults } from 'vue';
+
+      interface Props {
+        required: boolean;
+        optional?: boolean;
+      }
+
+      const props = withDefaults(defineProps<Props>(), {
+        optional: false,
+      });
+      </script>
+      `,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions: {
+        ...parserOptions,
+        parser: require.resolve('@typescript-eslint/parser')
+      }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      interface Props {
+        optional?: boolean;
+      }
+
+      const props = defineProps<Props>();
+      </script>
+      `,
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions: {
+        ...parserOptions,
+        parser: require.resolve('@typescript-eslint/parser')
+      }
     }
   ],
 
@@ -503,7 +550,7 @@ ruleTester.run('require-default-prop', rule, {
             code: `
             <script setup lang="ts">
             interface Props {
-              foo: number
+              foo?: number
             }
             withDefaults(defineProps<Props>(), {bar:42})
             </script>
diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js
index df45da856..b4a052090 100644
--- a/tests/lib/utils/index.js
+++ b/tests/lib/utils/index.js
@@ -1,13 +1,13 @@
 'use strict'
 
-const babelEslint = require('babel-eslint')
 const espree = require('espree')
 const utils = require('../../../lib/utils/index')
 const assert = require('assert')
 
 describe('getComputedProperties', () => {
   const parse = function (code) {
-    return babelEslint.parse(code).body[0].declarations[0].init
+    return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+      .init
   }
 
   it('should return empty array when there is no computed property', () => {
@@ -112,7 +112,8 @@ describe('getComputedProperties', () => {
 
 describe('getStaticPropertyName', () => {
   const parse = function (code) {
-    return babelEslint.parse(code).body[0].declarations[0].init
+    return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+      .init
   }
 
   it('should parse property expression with identifier', () => {
@@ -137,7 +138,8 @@ describe('getStaticPropertyName', () => {
 
 describe('getStringLiteralValue', () => {
   const parse = function (code) {
-    return babelEslint.parse(code).body[0].declarations[0].init
+    return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+      .init
   }
 
   it('should parse literal', () => {
@@ -275,7 +277,8 @@ describe('getMemberChaining', () => {
 
 describe('getRegisteredComponents', () => {
   const parse = function (code) {
-    return babelEslint.parse(code).body[0].declarations[0].init
+    return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0]
+      .init
   }
 
   it('should return empty array when there are no components registered', () => {
@@ -335,7 +338,8 @@ describe('getRegisteredComponents', () => {
 
 describe('getComponentProps', () => {
   const parse = function (code) {
-    const data = babelEslint.parse(code).body[0].declarations[0].init
+    const data = espree.parse(code, { ecmaVersion: 2020 }).body[0]
+      .declarations[0].init
     return utils.getComponentProps(data)
   }