From 0141f9a347292ed4f7bb371dae6219231a240ad7 Mon Sep 17 00:00:00 2001 From: ota Date: Mon, 15 Jun 2020 12:29:36 +0900 Subject: [PATCH] Add `"v-model-argument"` and `"v-model-custom-modifiers"` to the syntax checked by the `vue/no-unsupported-features` rule. --- docs/rules/no-unsupported-features.md | 40 +++++++++- lib/rules/no-unsupported-features.js | 66 ++++++++++++----- .../syntaxes/dynamic-directive-arguments.js | 2 +- lib/rules/syntaxes/scope-attribute.js | 2 +- lib/rules/syntaxes/slot-attribute.js | 2 +- lib/rules/syntaxes/slot-scope-attribute.js | 1 + .../v-bind-prop-modifier-shorthand.js | 2 +- lib/rules/syntaxes/v-model-argument.js | 23 ++++++ .../syntaxes/v-model-custom-modifiers.js | 33 +++++++++ lib/rules/syntaxes/v-slot.js | 2 +- lib/utils/index.js | 14 ++-- .../v-model-argument.js | 59 +++++++++++++++ .../v-model-custom-modifiers.js | 73 +++++++++++++++++++ 13 files changed, 288 insertions(+), 31 deletions(-) create mode 100644 lib/rules/syntaxes/v-model-argument.js create mode 100644 lib/rules/syntaxes/v-model-custom-modifiers.js create mode 100644 tests/lib/rules/no-unsupported-features/v-model-argument.js create mode 100644 tests/lib/rules/no-unsupported-features/v-model-custom-modifiers.js 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 + } + ] + } + ] +})