Skip to content

Commit 0c80259

Browse files
authored
⭐️New: Add vue/no-unsupported-features rule (#841)
* ⭐️New: Add vue/no-unsupported-features rule * Change to autofix
1 parent a5fd31e commit 0c80259

13 files changed

+1085
-0
lines changed

Diff for: docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ For example:
159159
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
160160
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
161161
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
162+
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
162163
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
163164
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
164165
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | |

Diff for: docs/rules/no-unsupported-features.md

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/no-unsupported-features
5+
description: disallow unsupported Vue.js syntax on the specified version
6+
---
7+
# vue/no-unsupported-features
8+
> disallow unsupported Vue.js syntax on the specified version
9+
10+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
11+
12+
## :book: Rule Details
13+
14+
This rule reports unsupported Vue.js syntax on the specified version.
15+
16+
## :wrench: Options
17+
18+
```json
19+
{
20+
"vue/no-unsupported-features": ["error", {
21+
"version": "^2.6.0",
22+
"ignores": []
23+
}]
24+
}
25+
```
26+
27+
- `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.
28+
- `ignores` ... You can use this `ignores` option to ignore the given features.
29+
The `"ignores"` option accepts an array of the following strings.
30+
- Vue.js 2.6.0+
31+
- `"dynamic-directive-arguments"` ... [dynamic directive arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments).
32+
- `"v-slot"` ... [v-slot](https://vuejs.org/v2/api/#v-slot) directive.
33+
- Vue.js 2.5.0+
34+
- `"slot-scope-attribute"` ... [slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated) attributes.
35+
- Vue.js `">=2.6.0-beta.1 <=2.6.0-beta.3"` or 2.6 custom build
36+
- `"v-bind-prop-modifier-shorthand"` ... [v-bind](https://vuejs.org/v2/api/#v-bind) with `.prop` modifier shorthand.
37+
38+
### `{"version": "^2.5.0"}`
39+
40+
<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.5.0'}]}">
41+
42+
```vue
43+
<template>
44+
<!-- ✓ GOOD -->
45+
<CustomComponent :foo="val" />
46+
<ListComponent>
47+
<template slot="name" slot-scope="props">
48+
{{ props.title }}
49+
</template>
50+
</ListComponent>
51+
52+
<!-- ✗ BAD -->
53+
<!-- dynamic directive arguments -->
54+
<CustomComponent :[foo]="val" />
55+
<ListComponent>
56+
<!-- v-slot -->
57+
<template v-slot:name="props">
58+
{{ props.title }}
59+
</template>
60+
<template #name="props">
61+
{{ props.title }}
62+
</template>
63+
</ListComponent>
64+
</template>
65+
```
66+
67+
</eslint-code-block>
68+
69+
## :books: Further reading
70+
71+
- [Guide - Dynamic Arguments](https://vuejs.org/v2/guide/syntax.html#Dynamic-Arguments)
72+
- [API - v-slot](https://vuejs.org/v2/api/#v-slot)
73+
- [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)
78+
79+
## :mag: Implementation
80+
81+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unsupported-features.js)
82+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unsupported-features.js)

Diff for: lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module.exports = {
5454
'no-template-key': require('./rules/no-template-key'),
5555
'no-template-shadow': require('./rules/no-template-shadow'),
5656
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
57+
'no-unsupported-features': require('./rules/no-unsupported-features'),
5758
'no-unused-components': require('./rules/no-unused-components'),
5859
'no-unused-vars': require('./rules/no-unused-vars'),
5960
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),

Diff for: lib/rules/no-unsupported-features.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const { Range } = require('semver')
8+
const utils = require('../utils')
9+
10+
const FEATURES = {
11+
// Vue.js 2.5.0+
12+
'slot-scope-attribute': require('./syntaxes/slot-scope-attribute'),
13+
// Vue.js 2.6.0+
14+
'dynamic-directive-arguments': require('./syntaxes/dynamic-directive-arguments'),
15+
'v-slot': require('./syntaxes/v-slot'),
16+
17+
// >=2.6.0-beta.1 <=2.6.0-beta.3
18+
'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand')
19+
}
20+
21+
const cache = new Map()
22+
/**
23+
* Get the `semver.Range` object of a given range text.
24+
* @param {string} x The text expression for a semver range.
25+
* @returns {Range|null} The range object of a given range text.
26+
* It's null if the `x` is not a valid range text.
27+
*/
28+
function getSemverRange (x) {
29+
const s = String(x)
30+
let ret = cache.get(s) || null
31+
32+
if (!ret) {
33+
try {
34+
ret = new Range(s)
35+
} catch (_error) {
36+
// Ignore parsing error.
37+
}
38+
cache.set(s, ret)
39+
}
40+
41+
return ret
42+
}
43+
44+
/**
45+
* Merge two visitors.
46+
* @param {Visitor} x The visitor which is assigned.
47+
* @param {Visitor} y The visitor which is assigning.
48+
* @returns {Visitor} `x`.
49+
*/
50+
function merge (x, y) {
51+
for (const key of Object.keys(y)) {
52+
if (typeof x[key] === 'function') {
53+
if (x[key]._handlers == null) {
54+
const fs = [x[key], y[key]]
55+
x[key] = node => fs.forEach(h => h(node))
56+
x[key]._handlers = fs
57+
} else {
58+
x[key]._handlers.push(y[key])
59+
}
60+
} else {
61+
x[key] = y[key]
62+
}
63+
}
64+
return x
65+
}
66+
67+
module.exports = {
68+
meta: {
69+
type: 'suggestion',
70+
docs: {
71+
description: 'disallow unsupported Vue.js syntax on the specified version',
72+
category: undefined,
73+
url: 'https://eslint.vuejs.org/rules/no-unsupported-features.html'
74+
},
75+
fixable: 'code',
76+
schema: [
77+
{
78+
type: 'object',
79+
properties: {
80+
version: {
81+
type: 'string'
82+
},
83+
ignores: {
84+
type: 'array',
85+
items: {
86+
enum: Object.keys(FEATURES)
87+
},
88+
uniqueItems: true
89+
}
90+
},
91+
additionalProperties: false
92+
}
93+
],
94+
messages: {
95+
// Vue.js 2.5.0+
96+
forbiddenSlotScopeAttribute: '`slot-scope` are not supported until Vue.js "2.5.0".',
97+
// Vue.js 2.6.0+
98+
forbiddenDynamicDirectiveArguments: 'Dynamic arguments are not supported until Vue.js "2.6.0".',
99+
forbiddenVSlot: '`v-slot` are not supported until Vue.js "2.6.0".',
100+
101+
// >=2.6.0-beta.1 <=2.6.0-beta.3
102+
forbiddenVBindPropModifierShorthand: '`.prop` shorthand are not supported except Vue.js ">=2.6.0-beta.1 <=2.6.0-beta.3".'
103+
}
104+
},
105+
create (context) {
106+
const { version, ignores } = Object.assign(
107+
{
108+
version: null,
109+
ignores: []
110+
},
111+
context.options[0] || {}
112+
)
113+
if (!version) {
114+
// version is not set.
115+
return {}
116+
}
117+
const versionRange = getSemverRange(version)
118+
119+
/**
120+
* Check whether a given case object is full-supported on the configured node version.
121+
* @param {{supported:string}} aCase The case object to check.
122+
* @returns {boolean} `true` if it's supporting.
123+
*/
124+
function isNotSupportingVersion (aCase) {
125+
if (typeof aCase.supported === 'function') {
126+
return !aCase.supported(versionRange)
127+
}
128+
return versionRange.intersects(getSemverRange(`<${aCase.supported}`))
129+
}
130+
const templateBodyVisitor = Object.keys(FEATURES)
131+
.filter(syntaxName => !ignores.includes(syntaxName))
132+
.filter(syntaxName => isNotSupportingVersion(FEATURES[syntaxName]))
133+
.reduce((result, syntaxName) => {
134+
const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context)
135+
if (visitor) {
136+
merge(result, visitor)
137+
}
138+
return result
139+
}, {})
140+
141+
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
142+
}
143+
}

Diff for: lib/rules/syntaxes/dynamic-directive-arguments.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
module.exports = {
7+
supported: '2.6.0',
8+
createTemplateBodyVisitor (context) {
9+
/**
10+
* Reports dynamic argument node
11+
* @param {VExpressionContainer} dinamicArgument node of dynamic argument
12+
* @returns {void}
13+
*/
14+
function reportDynamicArgument (dinamicArgument) {
15+
context.report({
16+
node: dinamicArgument,
17+
messageId: 'forbiddenDynamicDirectiveArguments'
18+
})
19+
}
20+
21+
return {
22+
'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer': reportDynamicArgument
23+
}
24+
}
25+
}

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

+33
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+
const { Range } = require('semver')
7+
const unsupported = new Range('<=2.5 || >=2.6.0')
8+
9+
module.exports = {
10+
// >=2.6.0-beta.1 <=2.6.0-beta.3
11+
supported: (versionRange) => {
12+
return !versionRange.intersects(unsupported)
13+
},
14+
createTemplateBodyVisitor (context) {
15+
/**
16+
* Reports `.prop` shorthand node
17+
* @param {VDirectiveKey} bindPropKey node of `.prop` shorthand
18+
* @returns {void}
19+
*/
20+
function reportPropModifierShorthand (bindPropKey) {
21+
context.report({
22+
node: bindPropKey,
23+
messageId: 'forbiddenVBindPropModifierShorthand',
24+
// fix to use `:x.prop` (downgrade)
25+
fix: fixer => fixer.replaceText(bindPropKey, `:${bindPropKey.argument.rawName}.prop`)
26+
})
27+
}
28+
29+
return {
30+
"VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']": reportPropModifierShorthand
31+
}
32+
}
33+
}

Diff for: lib/rules/syntaxes/v-slot.js

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* @author Yosuke Ota
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
module.exports = {
7+
supported: '2.6.0',
8+
createTemplateBodyVisitor (context) {
9+
const sourceCode = context.getSourceCode()
10+
11+
/**
12+
* Checks whether the given node can convert to the `slot`.
13+
* @param {VAttribute} vSlotAttr node of `v-slot`
14+
* @returns {boolean} `true` if the given node can convert to the `slot`
15+
*/
16+
function canConvertToSlot (vSlotAttr) {
17+
if (vSlotAttr.parent.parent.name !== 'template') {
18+
return false
19+
}
20+
return true
21+
}
22+
/**
23+
* Convert to `slot` and `slot-scope`.
24+
* @param {object} fixer fixer
25+
* @param {VAttribute} vSlotAttr node of `v-slot`
26+
* @returns {*} fix data
27+
*/
28+
function fixVSlotToSlot (fixer, vSlotAttr) {
29+
const key = vSlotAttr.key
30+
if (key.modifiers.length) {
31+
// unknown modifiers
32+
return null
33+
}
34+
35+
const attrs = []
36+
const argument = key.argument
37+
if (argument) {
38+
if (argument.type === 'VIdentifier') {
39+
const name = argument.rawName
40+
attrs.push(`slot="${name}"`)
41+
} else if (argument.type === 'VExpressionContainer' && argument.expression) {
42+
const expression = sourceCode.getText(argument.expression)
43+
attrs.push(`:slot="${expression}"`)
44+
} else {
45+
// unknown or syntax error
46+
return null
47+
}
48+
}
49+
const scopedValueNode = vSlotAttr.value
50+
if (scopedValueNode) {
51+
attrs.push(
52+
`slot-scope=${sourceCode.getText(scopedValueNode)}`
53+
)
54+
}
55+
if (!attrs.length) {
56+
attrs.push('slot') // useless
57+
}
58+
return fixer.replaceText(vSlotAttr, attrs.join(' '))
59+
}
60+
/**
61+
* Reports `v-slot` node
62+
* @param {VAttribute} vSlotAttr node of `v-slot`
63+
* @returns {void}
64+
*/
65+
function reportVSlot (vSlotAttr) {
66+
context.report({
67+
node: vSlotAttr.key,
68+
messageId: 'forbiddenVSlot',
69+
// fix to use `slot` (downgrade)
70+
fix: fixer => {
71+
if (!canConvertToSlot(vSlotAttr)) {
72+
return null
73+
}
74+
return fixVSlotToSlot(fixer, vSlotAttr)
75+
}
76+
})
77+
}
78+
79+
return {
80+
"VAttribute[directive=true][key.name.name='slot']": reportVSlot
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)