Skip to content

Commit 04f83ba

Browse files
authored
Add "v-model-argument" and "v-model-custom-modifiers" to the syntax checked by the vue/no-unsupported-features rule. (#1212)
1 parent a12f2d9 commit 04f83ba

13 files changed

+288
-31
lines changed

docs/rules/no-unsupported-features.md

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ This rule reports unsupported Vue.js syntax on the specified version.
2727
- `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.
2828
- `ignores` ... You can use this `ignores` option to ignore the given features.
2929
The `"ignores"` option accepts an array of the following strings.
30+
- Vue.js 3.0.0+
31+
- `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
32+
- `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
3033
- Vue.js 2.6.0+
3134
- `"dynamic-directive-arguments"` ... [dynamic directive arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments).
3235
- `"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.
3538
- Vue.js `">=2.6.0-beta.1 <=2.6.0-beta.3"` or 2.6 custom build
3639
- `"v-bind-prop-modifier-shorthand"` ... [v-bind](https://vuejs.org/v2/api/#v-bind) with `.prop` modifier shorthand.
3740

41+
### `{"version": "^2.6.0"}`
42+
43+
<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.6.0'}]}">
44+
45+
```vue
46+
<template>
47+
<!-- ✓ GOOD -->
48+
<MyInput v-bind:foo.sync="val" />
49+
50+
<!-- ✗ BAD -->
51+
<!-- argument on `v-model` -->
52+
<MyInput v-model:foo="val" />
53+
<!-- custom modifiers on `v-model` -->
54+
<MyComp v-model.foo.bar="text" />
55+
</template>
56+
```
57+
58+
</eslint-code-block>
59+
3860
### `{"version": "^2.5.0"}`
3961

4062
<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.5.0'}]}">
@@ -71,10 +93,20 @@ The `"ignores"` option accepts an array of the following strings.
7193
- [Guide - Dynamic Arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments)
7294
- [API - v-slot](https://vuejs.org/v2/api/#v-slot)
7395
- [API - slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated)
74-
- [Vue RFCs - 0001-new-slot-syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md)
75-
- [Vue RFCs - 0002-slot-syntax-shorthand](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md)
76-
- [Vue RFCs - 0003-dynamic-directive-arguments](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md)
77-
- [Vue RFCs - v-bind .prop shorthand proposal](https://github.com/vuejs/rfcs/pull/18)
96+
- [Vue RFCs - 0001-new-slot-syntax]
97+
- [Vue RFCs - 0002-slot-syntax-shorthand]
98+
- [Vue RFCs - 0003-dynamic-directive-arguments]
99+
- [Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
100+
- [Vue RFCs - 0011-v-model-api-change]
101+
- [Vue RFCs - v-bind .prop shorthand proposal]
102+
103+
[Vue RFCs - 0001-new-slot-syntax]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
104+
[Vue RFCs - 0002-slot-syntax-shorthand]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0002-slot-syntax-shorthand.md
105+
[Vue RFCs - 0003-dynamic-directive-arguments]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0003-dynamic-directive-arguments.md
106+
[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
107+
[Vue RFCs - 0011-v-model-api-change]: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md
108+
109+
[Vue RFCs - v-bind .prop shorthand proposal]: https://github.com/vuejs/rfcs/pull/18
78110

79111
## :mag: Implementation
80112

lib/rules/no-unsupported-features.js

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,24 @@
77
const semver = require('semver')
88
const utils = require('../utils')
99

10+
/**
11+
* @typedef {object} SyntaxRule
12+
* @property {string | ((range: semver.Range) => boolean)} supported
13+
* @property { (context: RuleContext) => TemplateListener } [createTemplateBodyVisitor]
14+
* @property { (context: RuleContext) => RuleListener } [createScriptVisitor]
15+
*/
16+
1017
const FEATURES = {
1118
// Vue.js 2.5.0+
1219
'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
1320
// Vue.js 2.6.0+
1421
'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
1522
'v-slot': require('./syntaxes/v-slot'),
16-
1723
// >=2.6.0-beta.1 <=2.6.0-beta.3
18-
'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand')
24+
'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'),
25+
// Vue.js 3.0.0+
26+
'v-model-argument': require('./syntaxes/v-model-argument'),
27+
'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers')
1928
}
2029

2130
const cache = new Map()
@@ -77,10 +86,14 @@ module.exports = {
7786
forbiddenDynamicDirectiveArguments:
7887
'Dynamic arguments are not supported until Vue.js "2.6.0".',
7988
forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
80-
8189
// >=2.6.0-beta.1 <=2.6.0-beta.3
8290
forbiddenVBindPropModifierShorthand:
83-
'`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".'
91+
'`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".',
92+
// Vue.js 3.0.0+
93+
forbiddenVModelArgument:
94+
'Argument on `v-model` is not supported until Vue.js "3.0.0".',
95+
forbiddenVModelCustomModifiers:
96+
'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".'
8497
}
8598
},
8699
/** @param {RuleContext} context */
@@ -100,7 +113,7 @@ module.exports = {
100113

101114
/**
102115
* Check whether a given case object is full-supported on the configured node version.
103-
* @param { { supported?: string | ((range: semver.Range) => boolean) } } aCase The case object to check.
116+
* @param {SyntaxRule} aCase The case object to check.
104117
* @returns {boolean} `true` if it's supporting.
105118
*/
106119
function isNotSupportingVersion(aCase) {
@@ -110,19 +123,38 @@ module.exports = {
110123
return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
111124
}
112125

113-
const keys = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
126+
const syntaxNames = /** @type {(keyof FEATURES)[]} */ (Object.keys(
127+
FEATURES
128+
))
114129

115-
const templateBodyVisitor = keys
116-
.filter((syntaxName) => !ignores.includes(syntaxName))
117-
.filter((syntaxName) => isNotSupportingVersion(FEATURES[syntaxName]))
118-
.reduce((result, syntaxName) => {
119-
const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context)
120-
if (visitor) {
121-
return utils.compositingVisitors(result, visitor)
122-
}
123-
return result
124-
}, {})
130+
/** @type {TemplateListener} */
131+
let templateBodyVisitor = {}
132+
/** @type {RuleListener} */
133+
let scriptVisitor = {}
125134

126-
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
135+
for (const syntaxName of syntaxNames) {
136+
/** @type {SyntaxRule} */
137+
const syntax = FEATURES[syntaxName]
138+
if (ignores.includes(syntaxName) || !isNotSupportingVersion(syntax)) {
139+
continue
140+
}
141+
if (syntax.createTemplateBodyVisitor) {
142+
const visitor = syntax.createTemplateBodyVisitor(context)
143+
templateBodyVisitor = utils.compositingVisitors(
144+
templateBodyVisitor,
145+
visitor
146+
)
147+
}
148+
if (syntax.createScriptVisitor) {
149+
const visitor = syntax.createScriptVisitor(context)
150+
scriptVisitor = utils.compositingVisitors(scriptVisitor, visitor)
151+
}
152+
}
153+
154+
return utils.defineTemplateBodyVisitor(
155+
context,
156+
templateBodyVisitor,
157+
scriptVisitor
158+
)
127159
}
128160
}

lib/rules/syntaxes/dynamic-directive-arguments.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict'
66
module.exports = {
77
supported: '2.6.0',
8-
/** @param {RuleContext} context */
8+
/** @param {RuleContext} context @returns {TemplateListener} */
99
createTemplateBodyVisitor(context) {
1010
/**
1111
* Reports dynamic argument node

lib/rules/syntaxes/scope-attribute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict'
66
module.exports = {
77
deprecated: '2.5.0',
8-
/** @param {RuleContext} context */
8+
/** @param {RuleContext} context @returns {TemplateListener} */
99
createTemplateBodyVisitor(context) {
1010
/**
1111
* Reports `scope` node

lib/rules/syntaxes/slot-attribute.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict'
66
module.exports = {
77
deprecated: '2.6.0',
8-
/** @param {RuleContext} context */
8+
/** @param {RuleContext} context @returns {TemplateListener} */
99
createTemplateBodyVisitor(context) {
1010
const sourceCode = context.getSourceCode()
1111

lib/rules/syntaxes/slot-scope-attribute.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = {
1010
* @param {RuleContext} context
1111
* @param {object} option
1212
* @param {boolean} [option.fixToUpgrade]
13+
* @returns {TemplateListener}
1314
*/
1415
createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
1516
const sourceCode = context.getSourceCode()

lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = {
1212
supported: (versionRange) => {
1313
return !versionRange.intersects(unsupported)
1414
},
15-
/** @param {RuleContext} context */
15+
/** @param {RuleContext} context @returns {TemplateListener} */
1616
createTemplateBodyVisitor(context) {
1717
/**
1818
* Reports `.prop` shorthand node
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
module.exports = {
8+
supported: '3.0.0',
9+
/** @param {RuleContext} context @returns {TemplateListener} */
10+
createTemplateBodyVisitor(context) {
11+
return {
12+
/** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
13+
"VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
14+
node
15+
) {
16+
context.report({
17+
node: node.argument,
18+
messageId: 'forbiddenVModelArgument'
19+
})
20+
}
21+
}
22+
}
23+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
// ------------------------------------------------------------------------------
8+
// Helpers
9+
// ------------------------------------------------------------------------------
10+
11+
const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])
12+
13+
module.exports = {
14+
supported: '3.0.0',
15+
/** @param {RuleContext} context @returns {TemplateListener} */
16+
createTemplateBodyVisitor(context) {
17+
return {
18+
/** @param {VDirectiveKey} node */
19+
"VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
20+
node
21+
) {
22+
for (const modifier of node.modifiers) {
23+
if (!BUILTIN_MODIFIERS.has(modifier.name)) {
24+
context.report({
25+
node: modifier,
26+
messageId: 'forbiddenVModelCustomModifiers'
27+
})
28+
}
29+
}
30+
}
31+
}
32+
}
33+
}

lib/rules/syntaxes/v-slot.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
'use strict'
66
module.exports = {
77
supported: '2.6.0',
8-
/** @param {RuleContext} context */
8+
/** @param {RuleContext} context @returns {TemplateListener} */
99
createTemplateBodyVisitor(context) {
1010
const sourceCode = context.getSourceCode()
1111

lib/utils/index.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,7 @@ module.exports = {
12151215
* Find all functions which do not always return values
12161216
* @param {boolean} treatUndefinedAsUnspecified
12171217
* @param { (node: ESNode) => void } cb Callback function
1218+
* @returns {RuleListener}
12181219
*/
12191220
executeOnFunctionsWithoutReturn(treatUndefinedAsUnspecified, cb) {
12201221
/**
@@ -1580,23 +1581,26 @@ function defineTemplateBodyVisitor(
15801581
}
15811582

15821583
/**
1583-
* @param {RuleListener} visitor
1584-
* @param {...RuleListener} visitors
1585-
* @returns {RuleListener}
1584+
* @template T
1585+
* @param {T} visitor
1586+
* @param {...(TemplateListener | RuleListener | NodeListener)} visitors
1587+
* @returns {T}
15861588
*/
15871589
function compositingVisitors(visitor, ...visitors) {
15881590
for (const v of visitors) {
15891591
for (const key in v) {
1592+
// @ts-expect-error
15901593
if (visitor[key]) {
1594+
// @ts-expect-error
15911595
const o = visitor[key]
1592-
/** @param {any[]} args */
1596+
// @ts-expect-error
15931597
visitor[key] = (...args) => {
1594-
// @ts-expect-error
15951598
o(...args)
15961599
// @ts-expect-error
15971600
v[key](...args)
15981601
}
15991602
} else {
1603+
// @ts-expect-error
16001604
visitor[key] = v[key]
16011605
}
16021606
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../../lib/rules/no-unsupported-features')
9+
const utils = require('./utils')
10+
11+
const buildOptions = utils.optionsBuilder('v-model-argument', '^2.6.0')
12+
const tester = new RuleTester({
13+
parser: require.resolve('vue-eslint-parser'),
14+
parserOptions: {
15+
ecmaVersion: 2019
16+
}
17+
})
18+
19+
tester.run('no-unsupported-features/v-model-argument', rule, {
20+
valid: [
21+
{
22+
code: `
23+
<template>
24+
<MyInput v-model:foo="foo" />
25+
</template>`,
26+
options: buildOptions({ version: '^3.0.0' })
27+
},
28+
{
29+
code: `
30+
<template>
31+
<MyInput v-model="foo" />
32+
</template>`,
33+
options: buildOptions()
34+
},
35+
{
36+
code: `
37+
<template>
38+
<MyInput v-bind:foo.sync="foo" />
39+
</template>`,
40+
options: buildOptions()
41+
}
42+
],
43+
invalid: [
44+
{
45+
code: `
46+
<template>
47+
<MyInput v-model:foo="foo" />
48+
</template>`,
49+
options: buildOptions(),
50+
errors: [
51+
{
52+
message:
53+
'Argument on `v-model` is not supported until Vue.js "3.0.0".',
54+
line: 3
55+
}
56+
]
57+
}
58+
]
59+
})

0 commit comments

Comments
 (0)