diff --git a/.changeset/lucky-waves-obey.md b/.changeset/lucky-waves-obey.md
new file mode 100644
index 000000000..402eaadb4
--- /dev/null
+++ b/.changeset/lucky-waves-obey.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-vue": patch
+---
+
+feat: extend `require-default-prop` and `require-valid-default-prop` to check `defineModel`
diff --git a/docs/rules/require-default-prop.md b/docs/rules/require-default-prop.md
index 78b35456c..7b6393873 100644
--- a/docs/rules/require-default-prop.md
+++ b/docs/rules/require-default-prop.md
@@ -62,6 +62,15 @@ export default {
}
}
+
```
diff --git a/docs/rules/require-valid-default-prop.md b/docs/rules/require-valid-default-prop.md
index c7b291c87..6afd8f049 100644
--- a/docs/rules/require-valid-default-prop.md
+++ b/docs/rules/require-valid-default-prop.md
@@ -81,6 +81,13 @@ export default {
}
}
+
```
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index eeb2ae9bb..b73c84dbc 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -218,6 +218,33 @@ module.exports = {
}
return Boolean(defaultsByAssignmentPatterns[prop.propName])
})
+ },
+ onDefineModelEnter(node, model) {
+ if (model.options && model.options.type === 'ObjectExpression') {
+ if (
+ propIsRequired(model.options) ||
+ propHasDefault(model.options) ||
+ model.options.properties.some(
+ (p) =>
+ p.type === 'Property' &&
+ utils.getStaticPropertyName(p) === 'type' &&
+ isValueNodeOfBooleanType(p.value)
+ )
+ ) {
+ return
+ }
+ context.report({
+ node: model.options,
+ messageId: 'missingDefault',
+ data: { propName: model.name.modelName }
+ })
+ } else {
+ context.report({
+ node: model.options || node,
+ messageId: 'missingDefault',
+ data: { propName: model.name.modelName }
+ })
+ }
}
}),
utils.executeOnVue(context, (obj) => {
diff --git a/lib/rules/require-valid-default-prop.ts b/lib/rules/require-valid-default-prop.ts
index fce3d904b..83b3b38ca 100644
--- a/lib/rules/require-valid-default-prop.ts
+++ b/lib/rules/require-valid-default-prop.ts
@@ -10,6 +10,7 @@ import type {
} from '../utils/index.js'
import utils from '../utils/index.js'
import { capitalize } from '../utils/casing.ts'
+import tsTypes from '../utils/ts-utils/ts-types.js'
const NATIVE_TYPES = new Set([
'String',
@@ -112,6 +113,10 @@ export default {
node: CallExpression
props: PropDefaultFunctionContext[]
}[] = []
+ const defineModelPropsContexts: {
+ node: CallExpression
+ props: PropDefaultFunctionContext[]
+ }[] = []
interface ScopeStack {
upper: ScopeStack | null
@@ -445,14 +450,24 @@ export default {
| FunctionDeclaration
| ArrowFunctionExpression
) {
- const data = scriptSetupPropsContexts.at(-1)
- if (!data || !scopeStack) {
+ if (!scopeStack) {
return
}
- for (const { default: defType } of data.props) {
- if (node.body === defType.functionBody) {
- scopeStack.returnTypes = defType.returnTypes
+ const propsData = scriptSetupPropsContexts.at(-1)
+ if (propsData) {
+ for (const { default: defType } of propsData.props) {
+ if (node.body === defType.functionBody) {
+ scopeStack.returnTypes = defType.returnTypes
+ }
+ }
+ }
+ const modelData = defineModelPropsContexts.at(-1)
+ if (modelData) {
+ for (const { default: defType } of modelData.props) {
+ if (node.body === defType.functionBody) {
+ scopeStack.returnTypes = defType.returnTypes
+ }
}
}
},
@@ -469,6 +484,72 @@ export default {
for (const returnType of defType.returnTypes) {
if (typeNames.has(returnType.type)) continue
+ report(returnType.node, prop, typeNames)
+ }
+ }
+ },
+ onDefineModelEnter(node, model) {
+ let syntheticProp:
+ | ComponentObjectProp
+ | ComponentInferTypeProp
+ | null = null
+ let defaultFromOptions: Property | null = null
+
+ if (model.typeNode) {
+ syntheticProp = {
+ type: 'infer-type',
+ propName: model.name.modelName,
+ node: model.typeNode,
+ required: false,
+ types: tsTypes.inferRuntimeTypeFromTypeNode(
+ context,
+ model.typeNode
+ )
+ }
+ if (model.options && model.options.type === 'ObjectExpression') {
+ defaultFromOptions = getPropertyNode(model.options, 'default')
+ }
+ } else if (
+ model.options &&
+ model.options.type === 'ObjectExpression'
+ ) {
+ syntheticProp = {
+ type: 'object',
+ propName: model.name.modelName,
+ key: model.options,
+ value: model.options,
+ // `node` is only accessed in report() when propName is null,
+ // which never occurs here since propName is always modelName.
+ node: node as any
+ }
+ }
+
+ if (!syntheticProp) {
+ return
+ }
+
+ const propContexts = processPropDefs([syntheticProp], function* () {
+ if (defaultFromOptions) {
+ yield {
+ src: 'defaultProperty',
+ expression: defaultFromOptions.value
+ }
+ }
+ })
+ defineModelPropsContexts.push({ node, props: propContexts })
+ },
+ onDefineModelExit() {
+ const data = defineModelPropsContexts.pop()
+ if (!data) {
+ return
+ }
+ for (const {
+ prop,
+ types: typeNames,
+ default: defType
+ } of data.props) {
+ for (const returnType of defType.returnTypes) {
+ if (typeNames.has(returnType.type)) continue
report(returnType.node, prop, typeNames)
}
}
diff --git a/tests/lib/rules/require-default-prop.test.ts b/tests/lib/rules/require-default-prop.test.ts
index 554f9de22..ea5d536f0 100644
--- a/tests/lib/rules/require-default-prop.test.ts
+++ b/tests/lib/rules/require-default-prop.test.ts
@@ -413,6 +413,106 @@ ruleTester.run('require-default-prop', rule, {
...languageOptions,
parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
}
+ },
+ // defineModel — valid cases
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ }
+ },
+ // defineModel — TypeScript syntax (valid)
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ }
}
],
@@ -738,6 +838,117 @@ ruleTester.run('require-default-prop', rule, {
line: 3
}
]
+ },
+ // defineModel — invalid cases
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: "Prop 'modelValue' requires default value to be set.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: "Prop 'modelValue' requires default value to be set.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: "Prop 'modelValue' requires default value to be set.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: "Prop 'count' requires default value to be set.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions
+ },
+ errors: [
+ {
+ message: "Prop 'modelValue' requires default value to be set.",
+ line: 3
+ }
+ ]
+ },
+ // defineModel — TypeScript syntax
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: {
+ parser: vueEslintParser,
+ ...languageOptions,
+ parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
+ },
+ errors: [
+ {
+ message: "Prop 'modelValue' requires default value to be set.",
+ line: 3
+ }
+ ]
}
]
})
diff --git a/tests/lib/rules/require-valid-default-prop.test.ts b/tests/lib/rules/require-valid-default-prop.test.ts
index 0009d1a31..cf2255e2e 100644
--- a/tests/lib/rules/require-valid-default-prop.test.ts
+++ b/tests/lib/rules/require-valid-default-prop.test.ts
@@ -340,6 +340,96 @@ ruleTester.run('require-valid-default-prop', rule, {
`,
...getTypeScriptFixtureTestOptions()
+ },
+ // defineModel — no default (nothing to check)
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ // defineModel — correct runtime type match
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser }
+ },
+ // defineModel — TypeScript syntax (valid)
+ {
+ code: `
+
+ `,
+ ...getTypeScriptFixtureTestOptions()
+ },
+ {
+ code: `
+
+ `,
+ ...getTypeScriptFixtureTestOptions()
+ },
+ {
+ code: `
+
+ `,
+ ...getTypeScriptFixtureTestOptions()
}
],
@@ -1224,6 +1314,105 @@ ruleTester.run('require-valid-default-prop', rule, {
}
],
...getTypeScriptFixtureTestOptions()
+ },
+ // defineModel — wrong type
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser },
+ errors: [
+ {
+ message:
+ "Type of the default value for 'modelValue' prop must be a boolean.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser },
+ errors: [
+ {
+ message:
+ "Type of the default value for 'count' prop must be a number.",
+ line: 3
+ }
+ ]
+ },
+ // defineModel — Array/Object must use factory
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser },
+ errors: [
+ {
+ message:
+ "Type of the default value for 'modelValue' prop must be a function.",
+ line: 3
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser },
+ errors: [
+ {
+ message:
+ "Type of the default value for 'modelValue' prop must be a function.",
+ line: 3
+ }
+ ]
+ },
+ // defineModel — factory returns wrong type
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ languageOptions: { parser: vueEslintParser },
+ errors: [
+ {
+ message:
+ "Type of the default value for 'modelValue' prop must be a string.",
+ line: 3
+ }
+ ]
+ },
+ // defineModel — TypeScript syntax (invalid)
+ {
+ code: `
+
+ `,
+ ...getTypeScriptFixtureTestOptions(),
+ errors: [
+ {
+ message:
+ "Type of the default value for 'modelValue' prop must be a string.",
+ line: 3
+ }
+ ]
}
]
})