diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md
index 5bb39148f..a9a8693ce 100644
--- a/docs/rules/no-unsupported-features.md
+++ b/docs/rules/no-unsupported-features.md
@@ -27,6 +27,9 @@ This rule reports unsupported Vue.js syntax on the specified version.
- `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required.
- `ignores` ... You can use this `ignores` option to ignore the given features.
The `"ignores"` option accepts an array of the following strings.
+ - Vue.js 3.0.0+
+ - `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
+ - `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
- Vue.js 2.6.0+
- `"dynamic-directive-arguments"` ... [dynamic directive arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments).
- `"v-slot"` ... [v-slot](https://vuejs.org/v2/api/#v-slot) directive.
@@ -35,6 +38,25 @@ The `"ignores"` option accepts an array of the following strings.
- Vue.js `">=2.6.0-beta.1 <=2.6.0-beta.3"` or 2.6 custom build
- `"v-bind-prop-modifier-shorthand"` ... [v-bind](https://vuejs.org/v2/api/#v-bind) with `.prop` modifier shorthand.
+### `{"version": "^2.6.0"}`
+
+
+
+```vue
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
### `{"version": "^2.5.0"}`
@@ -71,10 +93,20 @@ The `"ignores"` option accepts an array of the following strings.
- [Guide - Dynamic Arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments)
- [API - v-slot](https://vuejs.org/v2/api/#v-slot)
- [API - slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated)
-- [Vue RFCs - 0001-new-slot-syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md)
-- [Vue RFCs - 0002-slot-syntax-shorthand](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md)
-- [Vue RFCs - 0003-dynamic-directive-arguments](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md)
-- [Vue RFCs - v-bind .prop shorthand proposal](https://github.com/vuejs/rfcs/pull/18)
+- [Vue RFCs - 0001-new-slot-syntax]
+- [Vue RFCs - 0002-slot-syntax-shorthand]
+- [Vue RFCs - 0003-dynamic-directive-arguments]
+- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
+- [Vue RFCs - 0011-v-model-api-change]
+- [Vue RFCs - v-bind .prop shorthand proposal]
+
+[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
+[Vue RFCs - 0002-slot-syntax-shorthand]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md
+[Vue RFCs - 0003-dynamic-directive-arguments]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md
+[Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0005-replace-v-bind-sync-with-v-model-argument.md
+[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
+
+[Vue RFCs - v-bind .prop shorthand proposal]: https://github.com/vuejs/rfcs/pull/18
## :mag: Implementation
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index eaf9ce57a..468e718ec 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -7,15 +7,24 @@
const semver = require('semver')
const utils = require('../utils')
+/**
+ * @typedef {object} SyntaxRule
+ * @property {string | ((range: semver.Range) => boolean)} supported
+ * @property { (context: RuleContext) => TemplateListener } [createTemplateBodyVisitor]
+ * @property { (context: RuleContext) => RuleListener } [createScriptVisitor]
+ */
+
const FEATURES = {
// Vue.js 2.5.0+
'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
// Vue.js 2.6.0+
'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
'v-slot': require('./syntaxes/v-slot'),
-
// >=2.6.0-beta.1 <=2.6.0-beta.3
- 'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand')
+ 'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
+ // Vue.js 3.0.0+
+ 'v-model-argument': require('./syntaxes/v-model-argument'),
+ 'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers')
}
const cache = new Map()
@@ -77,10 +86,14 @@ module.exports = {
forbiddenDynamicDirectiveArguments:
'Dynamic arguments are not supported until Vue.js "2.6.0".',
forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
-
// >=2.6.0-beta.1 <=2.6.0-beta.3
forbiddenVBindPropModifierShorthand:
- '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".'
+ '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
+ // Vue.js 3.0.0+
+ forbiddenVModelArgument:
+ 'Argument on `v-model` is not supported until Vue.js "3.0.0".',
+ forbiddenVModelCustomModifiers:
+ 'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".'
}
},
/** @param {RuleContext} context */
@@ -100,7 +113,7 @@ module.exports = {
/**
* Check whether a given case object is full-supported on the configured node version.
- * @param { { supported?: string | ((range: semver.Range) => boolean) } } aCase The case object to check.
+ * @param {SyntaxRule} aCase The case object to check.
* @returns {boolean} `true` if it's supporting.
*/
function isNotSupportingVersion(aCase) {
@@ -110,19 +123,38 @@ module.exports = {
return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
}
- const keys = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
+ const syntaxNames = /** @type {(keyof FEATURES)[]} */ (Object.keys(
+ FEATURES
+ ))
- const templateBodyVisitor = keys
- .filter((syntaxName) => !ignores.includes(syntaxName))
- .filter((syntaxName) => isNotSupportingVersion(FEATURES[syntaxName]))
- .reduce((result, syntaxName) => {
- const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context)
- if (visitor) {
- return utils.compositingVisitors(result, visitor)
- }
- return result
- }, {})
+ /** @type {TemplateListener} */
+ let templateBodyVisitor = {}
+ /** @type {RuleListener} */
+ let scriptVisitor = {}
- return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
+ for (const syntaxName of syntaxNames) {
+ /** @type {SyntaxRule} */
+ const syntax = FEATURES[syntaxName]
+ if (ignores.includes(syntaxName) || !isNotSupportingVersion(syntax)) {
+ continue
+ }
+ if (syntax.createTemplateBodyVisitor) {
+ const visitor = syntax.createTemplateBodyVisitor(context)
+ templateBodyVisitor = utils.compositingVisitors(
+ templateBodyVisitor,
+ visitor
+ )
+ }
+ if (syntax.createScriptVisitor) {
+ const visitor = syntax.createScriptVisitor(context)
+ scriptVisitor = utils.compositingVisitors(scriptVisitor, visitor)
+ }
+ }
+
+ return utils.defineTemplateBodyVisitor(
+ context,
+ templateBodyVisitor,
+ scriptVisitor
+ )
}
}
diff --git a/lib/rules/syntaxes/dynamic-directive-arguments.js b/lib/rules/syntaxes/dynamic-directive-arguments.js
index 314bceeaa..595f70fb9 100644
--- a/lib/rules/syntaxes/dynamic-directive-arguments.js
+++ b/lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -5,7 +5,7 @@
'use strict'
module.exports = {
supported: '2.6.0',
- /** @param {RuleContext} context */
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports dynamic argument node
diff --git a/lib/rules/syntaxes/scope-attribute.js b/lib/rules/syntaxes/scope-attribute.js
index e2b0a697e..c1673c3cc 100644
--- a/lib/rules/syntaxes/scope-attribute.js
+++ b/lib/rules/syntaxes/scope-attribute.js
@@ -5,7 +5,7 @@
'use strict'
module.exports = {
deprecated: '2.5.0',
- /** @param {RuleContext} context */
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `scope` node
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index 12d6ad67a..bf84361fb 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -5,7 +5,7 @@
'use strict'
module.exports = {
deprecated: '2.6.0',
- /** @param {RuleContext} context */
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
const sourceCode = context.getSourceCode()
diff --git a/lib/rules/syntaxes/slot-scope-attribute.js b/lib/rules/syntaxes/slot-scope-attribute.js
index 6efa41552..e05be5c3f 100644
--- a/lib/rules/syntaxes/slot-scope-attribute.js
+++ b/lib/rules/syntaxes/slot-scope-attribute.js
@@ -10,6 +10,7 @@ module.exports = {
* @param {RuleContext} context
* @param {object} option
* @param {boolean} [option.fixToUpgrade]
+ * @returns {TemplateListener}
*/
createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
const sourceCode = context.getSourceCode()
diff --git a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
index 1374379f2..4038c81a5 100644
--- a/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
+++ b/lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -12,7 +12,7 @@ module.exports = {
supported: (versionRange) => {
return !versionRange.intersects(unsupported)
},
- /** @param {RuleContext} context */
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
/**
* Reports `.prop` shorthand node
diff --git a/lib/rules/syntaxes/v-model-argument.js b/lib/rules/syntaxes/v-model-argument.js
new file mode 100644
index 000000000..bba028e12
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-argument.js
@@ -0,0 +1,23 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+ supported: '3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ return {
+ /** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
+ "VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
+ node
+ ) {
+ context.report({
+ node: node.argument,
+ messageId: 'forbiddenVModelArgument'
+ })
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-model-custom-modifiers.js b/lib/rules/syntaxes/v-model-custom-modifiers.js
new file mode 100644
index 000000000..5630f9ad4
--- /dev/null
+++ b/lib/rules/syntaxes/v-model-custom-modifiers.js
@@ -0,0 +1,33 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
+
+module.exports = {
+ supported: '3.0.0',
+ /** @param {RuleContext} context @returns {TemplateListener} */
+ createTemplateBodyVisitor(context) {
+ return {
+ /** @param {VDirectiveKey} node */
+ "VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
+ node
+ ) {
+ for (const modifier of node.modifiers) {
+ if (!BUILTIN_MODIFIERS.has(modifier.name)) {
+ context.report({
+ node: modifier,
+ messageId: 'forbiddenVModelCustomModifiers'
+ })
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/rules/syntaxes/v-slot.js b/lib/rules/syntaxes/v-slot.js
index c3b5dfc0d..9d563a2dd 100644
--- a/lib/rules/syntaxes/v-slot.js
+++ b/lib/rules/syntaxes/v-slot.js
@@ -5,7 +5,7 @@
'use strict'
module.exports = {
supported: '2.6.0',
- /** @param {RuleContext} context */
+ /** @param {RuleContext} context @returns {TemplateListener} */
createTemplateBodyVisitor(context) {
const sourceCode = context.getSourceCode()
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 4e1725efa..a0710a8d9 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1215,6 +1215,7 @@ module.exports = {
* Find all functions which do not always return values
* @param {boolean} treatUndefinedAsUnspecified
* @param { (node: ESNode) => void } cb Callback function
+ * @returns {RuleListener}
*/
executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, cb) {
/**
@@ -1580,23 +1581,26 @@ function defineTemplateBodyVisitor(
}
/**
- * @param {RuleListener} visitor
- * @param {...RuleListener} visitors
- * @returns {RuleListener}
+ * @template T
+ * @param {T} visitor
+ * @param {...(TemplateListener | RuleListener | NodeListener)} visitors
+ * @returns {T}
*/
function compositingVisitors(visitor, ...visitors) {
for (const v of visitors) {
for (const key in v) {
+ // @ts-expect-error
if (visitor[key]) {
+ // @ts-expect-error
const o = visitor[key]
- /** @param {any[]} args */
+ // @ts-expect-error
visitor[key] = (...args) => {
- // @ts-expect-error
o(...args)
// @ts-expect-error
v[key](...args)
}
} else {
+ // @ts-expect-error
visitor[key] = v[key]
}
}
diff --git a/tests/lib/rules/no-unsupported-features/v-model-argument.js b/tests/lib/rules/no-unsupported-features/v-model-argument.js
new file mode 100644
index 000000000..ceb79784e
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-model-argument.js
@@ -0,0 +1,59 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-model-argument', '^2.6.0')
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2019
+ }
+})
+
+tester.run('no-unsupported-features/v-model-argument', rule, {
+ valid: [
+ {
+ code: `
+
+
+ `,
+ options: buildOptions({ version: '^3.0.0' })
+ },
+ {
+ code: `
+
+
+ `,
+ options: buildOptions()
+ },
+ {
+ code: `
+
+
+ `,
+ options: buildOptions()
+ }
+ ],
+ invalid: [
+ {
+ code: `
+
+
+ `,
+ options: buildOptions(),
+ errors: [
+ {
+ message:
+ 'Argument on `v-model` is not supported until Vue.js "3.0.0".',
+ line: 3
+ }
+ ]
+ }
+ ]
+})
diff --git a/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js b/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js
new file mode 100644
index 000000000..521272738
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js
@@ -0,0 +1,73 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder('v-model-custom-modifiers', '^2.6.0')
+const tester = new RuleTester({
+ parser: require.resolve('vue-eslint-parser'),
+ parserOptions: {
+ ecmaVersion: 2019
+ }
+})
+
+tester.run('no-unsupported-features/v-model-custom-modifiers', rule, {
+ valid: [
+ {
+ code: `
+
+
+ `,
+ options: buildOptions({ version: '^3.0.0' })
+ },
+ {
+ code: `
+
+
+ `,
+ options: buildOptions({ version: '^3.0.0' })
+ },
+ {
+ code: `
+
+
+ `,
+ options: buildOptions()
+ }
+ ],
+ invalid: [
+ {
+ code: `
+
+
+ `,
+ options: buildOptions(),
+ errors: [
+ {
+ message:
+ 'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
+ line: 3
+ }
+ ]
+ },
+ {
+ code: `
+
+
+ `,
+ options: buildOptions(),
+ errors: [
+ {
+ message:
+ 'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
+ line: 3
+ }
+ ]
+ }
+ ]
+})