-
-
Notifications
You must be signed in to change notification settings - Fork 666
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
⭐️New: Add vue/no-unsupported-features rule (#841)
* ⭐️New: Add vue/no-unsupported-features rule * Change to autofix
- Loading branch information
Showing
13 changed files
with
1,085 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
--- | ||
pageClass: rule-details | ||
sidebarDepth: 0 | ||
title: vue/no-unsupported-features | ||
description: disallow unsupported Vue.js syntax on the specified version | ||
--- | ||
# vue/no-unsupported-features | ||
> disallow unsupported Vue.js syntax on the specified version | ||
- :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. | ||
|
||
## :book: Rule Details | ||
|
||
This rule reports unsupported Vue.js syntax on the specified version. | ||
|
||
## :wrench: Options | ||
|
||
```json | ||
{ | ||
"vue/no-unsupported-features": ["error", { | ||
"version": "^2.6.0", | ||
"ignores": [] | ||
}] | ||
} | ||
``` | ||
|
||
- `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 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. | ||
- Vue.js 2.5.0+ | ||
- `"slot-scope-attribute"` ... [slot-scope](https://vuejs.org/v2/api/#slot-scope-deprecated) attributes. | ||
- 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.5.0"}` | ||
|
||
<eslint-code-block fix :rules="{'vue/no-unsupported-features': ['error', {'version': '^2.5.0'}]}"> | ||
|
||
```vue | ||
<template> | ||
<!-- ✓ GOOD --> | ||
<CustomComponent :foo="val" /> | ||
<ListComponent> | ||
<template slot="name" slot-scope="props"> | ||
{{ props.title }} | ||
</template> | ||
</ListComponent> | ||
<!-- ✗ BAD --> | ||
<!-- dynamic directive arguments --> | ||
<CustomComponent :[foo]="val" /> | ||
<ListComponent> | ||
<!-- v-slot --> | ||
<template v-slot:name="props"> | ||
{{ props.title }} | ||
</template> | ||
<template #name="props"> | ||
{{ props.title }} | ||
</template> | ||
</ListComponent> | ||
</template> | ||
``` | ||
|
||
</eslint-code-block> | ||
|
||
## :books: Further reading | ||
|
||
- [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) | ||
|
||
## :mag: Implementation | ||
|
||
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unsupported-features.js) | ||
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unsupported-features.js) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
/** | ||
* @author Yosuke Ota | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
'use strict' | ||
|
||
const { Range } = require('semver') | ||
const utils = require('../utils') | ||
|
||
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') | ||
} | ||
|
||
const cache = new Map() | ||
/** | ||
* Get the `semver.Range` object of a given range text. | ||
* @param {string} x The text expression for a semver range. | ||
* @returns {Range|null} The range object of a given range text. | ||
* It's null if the `x` is not a valid range text. | ||
*/ | ||
function getSemverRange (x) { | ||
const s = String(x) | ||
let ret = cache.get(s) || null | ||
|
||
if (!ret) { | ||
try { | ||
ret = new Range(s) | ||
} catch (_error) { | ||
// Ignore parsing error. | ||
} | ||
cache.set(s, ret) | ||
} | ||
|
||
return ret | ||
} | ||
|
||
/** | ||
* Merge two visitors. | ||
* @param {Visitor} x The visitor which is assigned. | ||
* @param {Visitor} y The visitor which is assigning. | ||
* @returns {Visitor} `x`. | ||
*/ | ||
function merge (x, y) { | ||
for (const key of Object.keys(y)) { | ||
if (typeof x[key] === 'function') { | ||
if (x[key]._handlers == null) { | ||
const fs = [x[key], y[key]] | ||
x[key] = node => fs.forEach(h => h(node)) | ||
x[key]._handlers = fs | ||
} else { | ||
x[key]._handlers.push(y[key]) | ||
} | ||
} else { | ||
x[key] = y[key] | ||
} | ||
} | ||
return x | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'disallow unsupported Vue.js syntax on the specified version', | ||
category: undefined, | ||
url: 'https://eslint.vuejs.org/rules/no-unsupported-features.html' | ||
}, | ||
fixable: 'code', | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
version: { | ||
type: 'string' | ||
}, | ||
ignores: { | ||
type: 'array', | ||
items: { | ||
enum: Object.keys(FEATURES) | ||
}, | ||
uniqueItems: true | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
], | ||
messages: { | ||
// Vue.js 2.5.0+ | ||
forbiddenSlotScopeAttribute: '`slot-scope` are not supported until Vue.js "2.5.0".', | ||
// Vue.js 2.6.0+ | ||
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".' | ||
} | ||
}, | ||
create (context) { | ||
const { version, ignores } = Object.assign( | ||
{ | ||
version: null, | ||
ignores: [] | ||
}, | ||
context.options[0] || {} | ||
) | ||
if (!version) { | ||
// version is not set. | ||
return {} | ||
} | ||
const versionRange = getSemverRange(version) | ||
|
||
/** | ||
* Check whether a given case object is full-supported on the configured node version. | ||
* @param {{supported:string}} aCase The case object to check. | ||
* @returns {boolean} `true` if it's supporting. | ||
*/ | ||
function isNotSupportingVersion (aCase) { | ||
if (typeof aCase.supported === 'function') { | ||
return !aCase.supported(versionRange) | ||
} | ||
return versionRange.intersects(getSemverRange(`<${aCase.supported}`)) | ||
} | ||
const templateBodyVisitor = Object.keys(FEATURES) | ||
.filter(syntaxName => !ignores.includes(syntaxName)) | ||
.filter(syntaxName => isNotSupportingVersion(FEATURES[syntaxName])) | ||
.reduce((result, syntaxName) => { | ||
const visitor = FEATURES[syntaxName].createTemplateBodyVisitor(context) | ||
if (visitor) { | ||
merge(result, visitor) | ||
} | ||
return result | ||
}, {}) | ||
|
||
return utils.defineTemplateBodyVisitor(context, templateBodyVisitor) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/** | ||
* @author Yosuke Ota | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
'use strict' | ||
module.exports = { | ||
supported: '2.6.0', | ||
createTemplateBodyVisitor (context) { | ||
/** | ||
* Reports dynamic argument node | ||
* @param {VExpressionContainer} dinamicArgument node of dynamic argument | ||
* @returns {void} | ||
*/ | ||
function reportDynamicArgument (dinamicArgument) { | ||
context.report({ | ||
node: dinamicArgument, | ||
messageId: 'forbiddenDynamicDirectiveArguments' | ||
}) | ||
} | ||
|
||
return { | ||
'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer': reportDynamicArgument | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/** | ||
* @author Yosuke Ota | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
'use strict' | ||
const { Range } = require('semver') | ||
const unsupported = new Range('<=2.5 || >=2.6.0') | ||
|
||
module.exports = { | ||
// >=2.6.0-beta.1 <=2.6.0-beta.3 | ||
supported: (versionRange) => { | ||
return !versionRange.intersects(unsupported) | ||
}, | ||
createTemplateBodyVisitor (context) { | ||
/** | ||
* Reports `.prop` shorthand node | ||
* @param {VDirectiveKey} bindPropKey node of `.prop` shorthand | ||
* @returns {void} | ||
*/ | ||
function reportPropModifierShorthand (bindPropKey) { | ||
context.report({ | ||
node: bindPropKey, | ||
messageId: 'forbiddenVBindPropModifierShorthand', | ||
// fix to use `:x.prop` (downgrade) | ||
fix: fixer => fixer.replaceText(bindPropKey, `:${bindPropKey.argument.rawName}.prop`) | ||
}) | ||
} | ||
|
||
return { | ||
"VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']": reportPropModifierShorthand | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/** | ||
* @author Yosuke Ota | ||
* See LICENSE file in root directory for full license. | ||
*/ | ||
'use strict' | ||
module.exports = { | ||
supported: '2.6.0', | ||
createTemplateBodyVisitor (context) { | ||
const sourceCode = context.getSourceCode() | ||
|
||
/** | ||
* Checks whether the given node can convert to the `slot`. | ||
* @param {VAttribute} vSlotAttr node of `v-slot` | ||
* @returns {boolean} `true` if the given node can convert to the `slot` | ||
*/ | ||
function canConvertToSlot (vSlotAttr) { | ||
if (vSlotAttr.parent.parent.name !== 'template') { | ||
return false | ||
} | ||
return true | ||
} | ||
/** | ||
* Convert to `slot` and `slot-scope`. | ||
* @param {object} fixer fixer | ||
* @param {VAttribute} vSlotAttr node of `v-slot` | ||
* @returns {*} fix data | ||
*/ | ||
function fixVSlotToSlot (fixer, vSlotAttr) { | ||
const key = vSlotAttr.key | ||
if (key.modifiers.length) { | ||
// unknown modifiers | ||
return null | ||
} | ||
|
||
const attrs = [] | ||
const argument = key.argument | ||
if (argument) { | ||
if (argument.type === 'VIdentifier') { | ||
const name = argument.rawName | ||
attrs.push(`slot="${name}"`) | ||
} else if (argument.type === 'VExpressionContainer' && argument.expression) { | ||
const expression = sourceCode.getText(argument.expression) | ||
attrs.push(`:slot="${expression}"`) | ||
} else { | ||
// unknown or syntax error | ||
return null | ||
} | ||
} | ||
const scopedValueNode = vSlotAttr.value | ||
if (scopedValueNode) { | ||
attrs.push( | ||
`slot-scope=${sourceCode.getText(scopedValueNode)}` | ||
) | ||
} | ||
if (!attrs.length) { | ||
attrs.push('slot') // useless | ||
} | ||
return fixer.replaceText(vSlotAttr, attrs.join(' ')) | ||
} | ||
/** | ||
* Reports `v-slot` node | ||
* @param {VAttribute} vSlotAttr node of `v-slot` | ||
* @returns {void} | ||
*/ | ||
function reportVSlot (vSlotAttr) { | ||
context.report({ | ||
node: vSlotAttr.key, | ||
messageId: 'forbiddenVSlot', | ||
// fix to use `slot` (downgrade) | ||
fix: fixer => { | ||
if (!canConvertToSlot(vSlotAttr)) { | ||
return null | ||
} | ||
return fixVSlotToSlot(fixer, vSlotAttr) | ||
} | ||
}) | ||
} | ||
|
||
return { | ||
"VAttribute[directive=true][key.name.name='slot']": reportVSlot | ||
} | ||
} | ||
} |
Oops, something went wrong.