Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⭐️New: Add vue/no-unsupported-features rule #841

Merged
merged 2 commits into from Dec 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -153,6 +153,7 @@ For example:
| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
| [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | |
| [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/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
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 @@ -48,6 +48,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
}
}
}
84 changes: 84 additions & 0 deletions lib/rules/syntaxes/slot-scope-attribute.js
@@ -0,0 +1,84 @@
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
module.exports = {
deprecated: '2.6.0',
supported: '2.5.0',
createTemplateBodyVisitor (context, { fixToUpgrade } = {}) {
const sourceCode = context.getSourceCode()

/**
* Checks whether the given node can convert to the `v-slot`.
* @param {VStartTag} startTag node of `<element v-slot ... >`
* @returns {boolean} `true` if the given node can convert to the `v-slot`
*/
function canConvertToVSlot (startTag) {
if (startTag.parent.name !== 'template') {
return false
}

const slotAttr = startTag.attributes
.find(attr => attr.directive === false && attr.key.name === 'slot')
if (slotAttr) {
// if the element have `slot` it can not be converted.
// Conversion of `slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}

const vBindSlotAttr = startTag.attributes
.find(attr =>
attr.directive === true &&
attr.key.name.name === 'bind' &&
attr.key.argument &&
attr.key.argument.name === 'slot')
if (vBindSlotAttr) {
// if the element have `v-bind:slot` it can not be converted.
// Conversion of `v-bind:slot` is done with `vue/no-deprecated-slot-attribute`.
return false
}
return true
}

/**
* Convert to `v-slot`.
* @param {object} fixer fixer
* @param {VAttribute | null} scopeAttr node of `slot-scope`
* @returns {*} fix data
*/
function fixSlotScopeToVSlot (fixer, scopeAttr) {
const scopeValue = scopeAttr && scopeAttr.value
? `=${sourceCode.getText(scopeAttr.value)}`
: ''

const replaceText = `v-slot${scopeValue}`
return fixer.replaceText(scopeAttr, replaceText)
}
/**
* Reports `slot-scope` node
* @param {VAttribute} scopeAttr node of `slot-scope`
* @returns {void}
*/
function reportSlotScope (scopeAttr) {
context.report({
node: scopeAttr.key,
messageId: 'forbiddenSlotScopeAttribute',
fix: fixToUpgrade
// fix to use `v-slot`
? (fixer) => {
const startTag = scopeAttr.parent
if (!canConvertToVSlot(startTag)) {
return null
}
return fixSlotScopeToVSlot(fixer, scopeAttr)
}
: null
})
}

return {
"VAttribute[directive=true][key.name.name='slot-scope']": reportSlotScope
}
}
}
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
}
}
}