Skip to content

Commit

Permalink
⭐️New: Add vue/no-unsupported-features rule (#841)
Browse files Browse the repository at this point in the history
* ⭐️New: Add vue/no-unsupported-features rule

* Change to autofix
  • Loading branch information
ota-meshi committed Dec 26, 2019
1 parent a5fd31e commit 0c80259
Show file tree
Hide file tree
Showing 13 changed files with 1,085 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -159,6 +159,7 @@ For example:
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | |
| [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | |
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | |
Expand Down
82 changes: 82 additions & 0 deletions docs/rules/no-unsupported-features.md
@@ -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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -54,6 +54,7 @@ module.exports = {
'no-template-key': require('./rules/no-template-key'),
'no-template-shadow': require('./rules/no-template-shadow'),
'no-textarea-mustache': require('./rules/no-textarea-mustache'),
'no-unsupported-features': require('./rules/no-unsupported-features'),
'no-unused-components': require('./rules/no-unused-components'),
'no-unused-vars': require('./rules/no-unused-vars'),
'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'),
Expand Down
143 changes: 143 additions & 0 deletions lib/rules/no-unsupported-features.js
@@ -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)
}
}
25 changes: 25 additions & 0 deletions lib/rules/syntaxes/dynamic-directive-arguments.js
@@ -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
}
}
}
33 changes: 33 additions & 0 deletions lib/rules/syntaxes/v-bind-prop-modifier-shorthand.js
@@ -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
}
}
}
83 changes: 83 additions & 0 deletions lib/rules/syntaxes/v-slot.js
@@ -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
}
}
}

0 comments on commit 0c80259

Please sign in to comment.