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

Issue:
False negative with vue/require-valid-default-prop on props with type assertion
<!--
  ❗Please don't ignore this template.❗

  If you ignore it, we're just going to respond asking you to fill it out, which wastes everyone's time.
  The more relevant information you can include, the faster we can find the issue and fix it without asking you for more info.
-->

<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**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.
<!-- If you do not read the FAQ and open an issue that is listed in the FAQ, we may silently close the issue. -->

**Tell us about your environment**

- **ESLint version:** 7.23.0
- **eslint-plugin-vue version:** 7.8.0 
- **Node version:** 14.16.0
- **Operating System:** Windows 10 Pro

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```js
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    "plugin:vue/essential",
    "eslint:recommended",
    "@vue/typescript/recommended",
    "@vue/prettier",
    "@vue/prettier/@typescript-eslint",
  ],
  parserOptions: {
    ecmaVersion: 2020,
  },
  rules: {},
};
```

**What did you do?**	

I annotated type to array prop with typescript according to the following documentations.
- https://vuejs.org/v2/guide/typescript.html#Annotating-Props
- https://v3.vuejs.org/guide/typescript-support.html#annotating-return-types

If the prop is an array, the default should be a function, but I set empty array directly by mistake.

<!-- Please include the actual source code causing the issue. -->
```vue
<script lang="ts">
import Vue from "vue";
import { PropType } from "vue";

export default Vue.extend({
  props: {
    foo: {
      type: Array as PropType<string[]>,
      default: [],
    },
  },
});
</script>
```

**What did you expect to happen?**

eslint reports type of the default value for 'foo' prop must be a function.

**What actually happened?**
<!--
  Please include the actual, raw output from ESLint.
  If you are only looking at the results of your editor extension, also check the CLI results.
-->

eslint did not report that. If there is no type assertion, it reports correctly.




Pull request:
Fix false negative with require-valid-default-prop on props with type assertion
fix #1468.

This PR ensure that eslint-plugin-vue reports `Type of thye default value for 'foo' prop must be function` in following case.
(It works the same for `Object` or `Function`.)

```vue
<script lang="ts">
import Vue from "vue";
import { PropType } from "vue";

export default Vue.extend({
  props: {
    foo: {
      type: Array as PropType<string[]>,
      default: [],
    },
  },
});
</script>
```

Note: In following case, type cheking based on annotated type of the return value of the default function will not work.

```vue
<script lang="ts">
import Vue from "vue";
import { PropType } from "vue";

export default Vue.extend({
  props: {
    foo: {
      type: Array as PropType<string[]>,
      default: (): number[] => [], // The function should return array of string, but this PR can't check it.
    },
  },
});
</script>
```

Patch:
diff --git a/docs/rules/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md
index 69bd7794..dac9d2d7 100644
--- a/docs/rules/max-attributes-per-line.md
+++ b/docs/rules/max-attributes-per-line.md
@@ -57,7 +57,10 @@ There is a configurable number of attributes that are acceptable in one-line cas
 ```json
 {
   "vue/max-attributes-per-line": ["error", {
-    "singleline": 1,
+    "singleline": {
+      "max": 1,
+      "allowFirstLine": true
+    },      
     "multiline": {
       "max": 1,
       "allowFirstLine": false
@@ -66,7 +69,8 @@ There is a configurable number of attributes that are acceptable in one-line cas
 }
 ```
 
-- `singleline` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
+- `singleline.max` (`number`) ... The number of maximum attributes per line when the opening tag is in a single line. Default is `1`.
+- `singleline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `true`.
 - `multiline.max` (`number`) ... The max number of attributes per line when the opening tag is in multiple lines. Default is `1`. This can be `{ multiline: 1 }` instead of `{ multiline: { max: 1 }}` if you don't configure `allowFirstLine` property.
 - `multiline.allowFirstLine` (`boolean`) ... If `true`, it allows attributes on the same line as that tag name. Default is `false`.
 
@@ -86,6 +90,24 @@ There is a configurable number of attributes that are acceptable in one-line cas
 
 </eslint-code-block>
 
+### `"singleline": 1, "allowFirstLine": false`
+
+<eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {singleline: { allowFirstLine: false }}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <MyComponent
+    lorem="1"
+  />
+
+  <!-- ✗ BAD -->
+  <MyComponent lorem="1" />
+</template>
+```
+
+</eslint-code-block>
+
 ### `"multiline": 2`
 
 <eslint-code-block fix :rules="{'vue/max-attributes-per-line': ['error', {multiline: 2}]}">
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index e379df47..d50a78d6 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -34,6 +34,9 @@ module.exports = {
                   max: {
                     type: 'number',
                     minimum: 1
+                  },
+                  allowFirstLine: {
+                    type: 'boolean'
                   }
                 },
                 additionalProperties: false
@@ -72,7 +75,8 @@ module.exports = {
     const configuration = parseOptions(context.options[0])
     const multilineMaximum = configuration.multiline
     const singlelinemMaximum = configuration.singleline
-    const canHaveFirstLine = configuration.allowFirstLine
+    const canHaveSinglelineFirstLine = configuration.singlelineAllowFirstLine
+    const canHaveMultilineFirstLine = configuration.multilineAllowFirstLine
     const template =
       context.parserServices.getTemplateBodyTokenStore &&
       context.parserServices.getTemplateBodyTokenStore()
@@ -83,16 +87,22 @@ module.exports = {
 
         if (!numberOfAttributes) return
 
-        if (
-          utils.isSingleLine(node) &&
-          numberOfAttributes > singlelinemMaximum
-        ) {
-          showErrors(node.attributes.slice(singlelinemMaximum))
+        if (utils.isSingleLine(node)) {
+          if (
+            !canHaveSinglelineFirstLine &&
+            node.attributes[0].loc.start.line === node.loc.start.line
+          ) {
+            showErrors([node.attributes[0]])
+          }
+
+          if (numberOfAttributes > singlelinemMaximum) {
+            showErrors(node.attributes.slice(singlelinemMaximum))
+          }
         }
 
         if (!utils.isSingleLine(node)) {
           if (
-            !canHaveFirstLine &&
+            !canHaveMultilineFirstLine &&
             node.attributes[0].loc.start.line === node.loc.start.line
           ) {
             showErrors([node.attributes[0]])
@@ -114,27 +124,36 @@ module.exports = {
     function parseOptions(options) {
       const defaults = {
         singleline: 1,
+        singlelineAllowFirstLine: true,
         multiline: 1,
-        allowFirstLine: false
+        multilineAllowFirstLine: false
       }
 
       if (options) {
         if (typeof options.singleline === 'number') {
           defaults.singleline = options.singleline
-        } else if (options.singleline && options.singleline.max) {
-          defaults.singleline = options.singleline.max
+        } else if (typeof options.singleline === 'object') {
+          if (typeof options.singleline.max === 'number') {
+            defaults.singleline = options.singleline.max
+          }
+
+          if (typeof options.singleline.allowFirstLine === 'boolean') {
+            defaults.singlelineAllowFirstLine =
+              options.singleline.allowFirstLine
+          }
         }
 
         if (options.multiline) {
           if (typeof options.multiline === 'number') {
             defaults.multiline = options.multiline
           } else if (typeof options.multiline === 'object') {
-            if (options.multiline.max) {
+            if (typeof options.multiline.max === 'number') {
               defaults.multiline = options.multiline.max
             }
 
-            if (options.multiline.allowFirstLine) {
-              defaults.allowFirstLine = options.multiline.allowFirstLine
+            if (typeof options.multiline.allowFirstLine === 'boolean') {
+              defaults.multilineAllowFirstLine =
+                options.multiline.allowFirstLine
             }
           }
         }
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index ef9453b2..e40cad86 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -49,10 +49,11 @@ function getPropertyNode(obj, name) {
 }
 
 /**
- * @param {Expression} node
+ * @param {Expression} targetNode
  * @returns {string[]}
  */
-function getTypes(node) {
+function getTypes(targetNode) {
+  const node = utils.skipTSAsExpression(targetNode)
   if (node.type === 'Identifier') {
     return [node.name]
   } else if (node.type === 'ArrayExpression') {


Test patch:
diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js
index fa721002..aedc6d6d 100644
--- a/tests/lib/rules/max-attributes-per-line.js
+++ b/tests/lib/rules/max-attributes-per-line.js
@@ -299,6 +299,19 @@ petname="Snoopy" extra="foo">
           line: 3
         }
       ]
+    },
+    {
+      code: `<template><component name="John Doe"></component></template>`,
+      options: [{ singleline: { allowFirstLine: false } }],
+      output: `<template><component
+name="John Doe"></component></template>`,
+      errors: [
+        {
+          message: "'name' should be on a new line.",
+          type: 'VAttribute',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js
index 58adccad..836f06d5 100644
--- a/tests/lib/rules/require-valid-default-prop.js
+++ b/tests/lib/rules/require-valid-default-prop.js
@@ -207,6 +207,51 @@ ruleTester.run('require-valid-default-prop', rule, {
         }
       }`,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Array as PropType<string[]>,
+              default: () => []
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Object as PropType<{ [key: number]: number }>,
+              default: () => {}
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Function as PropType<() => number>,
+              default: () => 10
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
     }
   ],
 
@@ -781,6 +826,51 @@ ruleTester.run('require-valid-default-prop', rule, {
       }`,
       parserOptions,
       errors: errorMessage('string')
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Array as PropType<string[]>,
+              default: []
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Object as PropType<{ [key: number]: number }>,
+              default: {}
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
+    },
+    {
+      filename: 'test.vue',
+      code: `export default Vue.extend({
+          props: {
+            foo: {
+              type: Function as PropType<() => number>,
+              default: 10
+            }
+          }
+        });
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parser: require.resolve('@typescript-eslint/parser'),
+      errors: errorMessage('function')
     }
   ]
 })