Skip to content

Commit

Permalink
Add "v-model-argument" and "v-model-custom-modifiers" to the synt…
Browse files Browse the repository at this point in the history
…ax checked by the `vue/no-unsupported-features` rule. (#1212)
  • Loading branch information
ota-meshi committed Jun 26, 2020
1 parent a12f2d9 commit 04f83ba
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 31 deletions.
40 changes: 36 additions & 4 deletions docs/rules/no-unsupported-features.md
Expand Up @@ -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.
Expand All @@ -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"}`

<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.6.0'}]}">

```vue
<template>
<!-- ✓ GOOD -->
<MyInput v-bind:foo.sync="val" />
<!-- ✗ BAD -->
<!-- argument on `v-model` -->
<MyInput v-model:foo="val" />
<!-- custom modifiers on `v-model` -->
<MyComp v-model.foo.bar="text" />
</template>
```

</eslint-code-block>

### `{"version": "^2.5.0"}`

<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.5.0'}]}">
Expand Down Expand Up @@ -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

Expand Down
66 changes: 49 additions & 17 deletions lib/rules/no-unsupported-features.js
Expand Up @@ -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()
Expand Down Expand Up @@ -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 */
Expand All @@ -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) {
Expand All @@ -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
)
}
}
2 changes: 1 addition & 1 deletion lib/rules/syntaxes/dynamic-directive-arguments.js
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/syntaxes/scope-attribute.js
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/syntaxes/slot-attribute.js
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions lib/rules/syntaxes/slot-scope-attribute.js
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions 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'
})
}
}
}
}
33 changes: 33 additions & 0 deletions 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'
})
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion lib/rules/syntaxes/v-slot.js
Expand Up @@ -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()

Expand Down
14 changes: 9 additions & 5 deletions lib/utils/index.js
Expand Up @@ -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) {
/**
Expand Down Expand Up @@ -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]
}
}
Expand Down
59 changes: 59 additions & 0 deletions 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: `
<template>
<MyInput v-model:foo="foo" />
</template>`,
options: buildOptions({ version: '^3.0.0' })
},
{
code: `
<template>
<MyInput v-model="foo" />
</template>`,
options: buildOptions()
},
{
code: `
<template>
<MyInput v-bind:foo.sync="foo" />
</template>`,
options: buildOptions()
}
],
invalid: [
{
code: `
<template>
<MyInput v-model:foo="foo" />
</template>`,
options: buildOptions(),
errors: [
{
message:
'Argument on `v-model` is not supported until Vue.js "3.0.0".',
line: 3
}
]
}
]
})

0 comments on commit 04f83ba

Please sign in to comment.