diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md index dfb73e851..8665e9b83 100644 --- a/docs/rules/no-unsupported-features.md +++ b/docs/rules/no-unsupported-features.md @@ -29,6 +29,8 @@ This rule reports unsupported Vue.js syntax on the specified version. - `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 3.1.0+ + - `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is) - Vue.js 3.0.0+ - `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument] - `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change] diff --git a/lib/rules/no-deprecated-html-element-is.js b/lib/rules/no-deprecated-html-element-is.js index e07f493a5..8382bb5c0 100644 --- a/lib/rules/no-deprecated-html-element-is.js +++ b/lib/rules/no-deprecated-html-element-is.js @@ -31,16 +31,34 @@ module.exports = { }, /** @param {RuleContext} context */ create(context) { + /** @param {VElement} node */ + function isValidElement(node) { + return ( + !utils.isHtmlWellKnownElementName(node.rawName) && + !utils.isSvgWellKnownElementName(node.rawName) + ) + } return utils.defineTemplateBodyVisitor(context, { - /** @param {VDirective | VAttribute} node */ - "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=false][key.name='is']"( + /** @param {VDirective} node */ + "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"( node ) { - const element = node.parent.parent - if ( - !utils.isHtmlWellKnownElementName(element.rawName) && - !utils.isSvgWellKnownElementName(element.rawName) - ) { + if (isValidElement(node.parent.parent)) { + return + } + context.report({ + node, + loc: node.loc, + messageId: 'unexpected' + }) + }, + /** @param {VAttribute} node */ + "VAttribute[directive=false][key.name='is']"(node) { + if (isValidElement(node.parent.parent)) { + return + } + if (node.value && node.value.value.startsWith('vue:')) { + // Usage on native elements 3.1+ return } context.report({ diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js index 6e6cb2ada..07d9e800a 100644 --- a/lib/rules/no-unregistered-components.js +++ b/lib/rules/no-unregistered-components.js @@ -119,11 +119,14 @@ module.exports = { /** @param {VAttribute} node */ "VAttribute[directive=false][key.name='is']"(node) { if ( - !node.value || // `` - utils.isHtmlWellKnownElementName(node.value.value) + !node.value // `` ) return - usedComponentNodes.push({ node, name: node.value.value }) + const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+ + ? node.value.value.slice(4) + : node.value.value + if (utils.isHtmlWellKnownElementName(value)) return + usedComponentNodes.push({ node, name: value }) }, /** @param {VElement} node */ "VElement[name='template'][parent.type='VDocumentFragment']:exit"() { diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index 9eb00be0e..2d4caf3e2 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -25,7 +25,9 @@ const FEATURES = { // Vue.js 3.0.0+ 'v-model-argument': require('./syntaxes/v-model-argument'), 'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'), - 'v-is': require('./syntaxes/v-is') + 'v-is': require('./syntaxes/v-is'), + // Vue.js 3.1.0+ + 'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix') } const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES)) @@ -97,7 +99,9 @@ module.exports = { 'Argument on `v-model` is not supported until Vue.js "3.0.0".', forbiddenVModelCustomModifiers: 'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".', - forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".' + forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".', + forbiddenIsAttributeWithVuePrefix: + '`is="vue:"` are not supported until Vue.js "3.1.0".' } }, /** @param {RuleContext} context */ diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js index e067e3eb0..8497dd961 100644 --- a/lib/rules/no-unused-components.js +++ b/lib/rules/no-unused-components.js @@ -88,7 +88,10 @@ module.exports = { if (!node.value) { return } - usedComponents.add(node.value.value) + const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+ + ? node.value.value.slice(4) + : node.value.value + usedComponents.add(value) }, /** @param {VElement} node */ "VElement[name='template']"(node) { diff --git a/lib/rules/syntaxes/is-attribute-with-vue-prefix.js b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js new file mode 100644 index 000000000..9fd2afdd0 --- /dev/null +++ b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js @@ -0,0 +1,25 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +module.exports = { + supported: '>=3.1.0', + /** @param {RuleContext} context @returns {TemplateListener} */ + createTemplateBodyVisitor(context) { + return { + /** @param {VAttribute} node */ + "VAttribute[directive=false][key.name='is']"(node) { + if (!node.value) { + return + } + if (node.value.value.startsWith('vue:')) { + context.report({ + node: node.value, + messageId: 'forbiddenIsAttributeWithVuePrefix' + }) + } + } + } + } +} diff --git a/lib/rules/syntaxes/v-is.js b/lib/rules/syntaxes/v-is.js index 9aeeb8d7e..f5eed36d5 100644 --- a/lib/rules/syntaxes/v-is.js +++ b/lib/rules/syntaxes/v-is.js @@ -4,6 +4,7 @@ */ 'use strict' module.exports = { + deprecated: '3.1.0', supported: '>=3.0.0', /** @param {RuleContext} context @returns {TemplateListener} */ createTemplateBodyVisitor(context) { diff --git a/tests/lib/rules/no-deprecated-html-element-is.js b/tests/lib/rules/no-deprecated-html-element-is.js index 961644946..9b7cfdb23 100644 --- a/tests/lib/rules/no-deprecated-html-element-is.js +++ b/tests/lib/rules/no-deprecated-html-element-is.js @@ -37,6 +37,12 @@ ruleTester.run('no-deprecated-html-element-is', rule, { { filename: 'test.vue', code: '' + }, + + // is="vue:xxx" + { + filename: 'test.vue', + code: '' } ], diff --git a/tests/lib/rules/no-unregistered-components.js b/tests/lib/rules/no-unregistered-components.js index 27739b78a..6a51bf494 100644 --- a/tests/lib/rules/no-unregistered-components.js +++ b/tests/lib/rules/no-unregistered-components.js @@ -446,6 +446,19 @@ tester.run('no-unregistered-components', rule, { ` }, + { + filename: 'test.vue', + code: ` + + + ` + }, { filename: 'test.vue', code: ` @@ -706,6 +719,48 @@ tester.run('no-unregistered-components', rule, { line: 3 } ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'The "foo" component has been used but not registered.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'The "foo" component has been used but not registered.', + line: 3 + } + ] } ] }) diff --git a/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js b/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js new file mode 100644 index 000000000..adcc64835 --- /dev/null +++ b/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js @@ -0,0 +1,64 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../../lib/rules/no-unsupported-features') +const utils = require('./utils') + +const buildOptions = utils.optionsBuilder( + 'is-attribute-with-vue-prefix', + '^3.1.0' +) +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019 + } +}) + +tester.run('no-unsupported-features/is-attribute-with-vue-prefix', rule, { + valid: [ + { + code: ` + `, + options: buildOptions() + }, + { + code: ` + `, + options: buildOptions({ version: '^2.5.0' }) + }, + { + code: ` + `, + options: buildOptions({ + version: '^2.5.0', + ignores: ['is-attribute-with-vue-prefix'] + }) + } + ], + invalid: [ + { + code: ` + `, + options: buildOptions({ version: '^3.0.0' }), + errors: [ + { + message: '`is="vue:"` are not supported until Vue.js "3.1.0".', + line: 3 + } + ] + } + ] +}) diff --git a/tests/lib/rules/no-unused-components.js b/tests/lib/rules/no-unused-components.js index 02cb1a3cf..f63b75f13 100644 --- a/tests/lib/rules/no-unused-components.js +++ b/tests/lib/rules/no-unused-components.js @@ -486,6 +486,21 @@ tester.run('no-unused-components', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + + ` } ], invalid: [