From 4c502a5bfa6d972d9454ad76cef18af6b3a8740c Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 13 May 2023 10:43:18 +0900 Subject: [PATCH 1/4] Add support for `defineOptions` and `defineSlots` to `vue/no-unsupported-features` rule --- docs/rules/no-unsupported-features.md | 5 ++ lib/rules/no-restricted-component-options.js | 24 ++++++-- lib/rules/no-unsupported-features.js | 12 +++- lib/rules/syntaxes/define-options.js | 22 +++++++ lib/rules/syntaxes/define-slots.js | 22 +++++++ .../rules/no-restricted-component-options.js | 16 +++++ .../no-unsupported-features/define-options.js | 59 +++++++++++++++++++ .../no-unsupported-features/define-slots.js | 59 +++++++++++++++++++ 8 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 lib/rules/syntaxes/define-options.js create mode 100644 lib/rules/syntaxes/define-slots.js create mode 100644 tests/lib/rules/no-unsupported-features/define-options.js create mode 100644 tests/lib/rules/no-unsupported-features/define-slots.js diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md index cd6c3dcf9..2e7d67501 100644 --- a/docs/rules/no-unsupported-features.md +++ b/docs/rules/no-unsupported-features.md @@ -29,6 +29,9 @@ 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.3.0+ + - `"define-slots"` ... `defineSlots()` macro. + - `"define-options"` ... `defineOptions()` macro. - Vue.js 3.2.0+ - `"v-memo"` ... [v-memo](https://vuejs.org/api/built-in-directives.html#v-memo) directive. - `"v-bind-prop-modifier-shorthand"` ... `v-bind` with `.prop` modifier shorthand. @@ -100,6 +103,8 @@ The `"ignores"` option accepts an array of the following strings. ## :books: Further Reading +- [API - defineOptions()](https://vuejs.org/api/sfc-script-setup.html#defineoptions) +- [API - defineSlots()](https://vuejs.org/api/sfc-script-setup.html#defineslots) - [API - v-memo](https://vuejs.org/api/built-in-directives.html#v-memo) - [API - v-is](https://v3.vuejs.org/api/directives.html#v-is) - [API - v-is (Old)](https://github.com/vuejs/docs-next/blob/008613756c3d781128d96b64a2d27f7598f8f548/src/api/directives.md#v-is) diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js index f50f091d5..5af68053c 100644 --- a/lib/rules/no-restricted-component-options.js +++ b/lib/rules/no-restricted-component-options.js @@ -173,13 +173,25 @@ module.exports = { /** @type {ParsedOption[]} */ const options = context.options.map(parseOption) - return utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - for (const option of options) { - verify(node, option.test, option.message) + return utils.compositingVisitors( + utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + for (const option of options) { + verify(node, option.test, option.message) + } } - } - }) + }), + utils.defineScriptSetupVisitor(context, { + onDefineOptionsEnter(node) { + if (node.arguments.length === 0) return + const define = node.arguments[0] + if (define.type !== 'ObjectExpression') return + for (const option of options) { + verify(define, option.test, option.message) + } + } + }) + ) /** * @param {ObjectExpression} node diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js index 803a62ca5..0bcd61a55 100644 --- a/lib/rules/no-unsupported-features.js +++ b/lib/rules/no-unsupported-features.js @@ -32,7 +32,10 @@ const FEATURES = { // Vue.js 3.2.0+ 'v-memo': require('./syntaxes/v-memo'), 'v-bind-prop-modifier-shorthand': require('./syntaxes/v-bind-prop-modifier-shorthand'), - 'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier') + 'v-bind-attr-modifier': require('./syntaxes/v-bind-attr-modifier'), + // Vue.js 3.3.0+ + 'define-options': require('./syntaxes/define-options'), + 'define-slots': require('./syntaxes/define-slots') } const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES)) @@ -115,7 +118,12 @@ module.exports = { forbiddenVBindPropModifierShorthand: '`.prop` shorthand are not supported until Vue.js "3.2.0".', forbiddenVBindAttrModifier: - '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".' + '`.attr` modifiers on `v-bind` are not supported until Vue.js "3.2.0".', + // Vue.js 3.3.0+ + forbiddenDefineOptions: + '`defineOptions()` macros are not supported until Vue.js "3.3.0".', + forbiddenDefineSlots: + '`defineSlots()` macros are not supported until Vue.js "3.3.0".' } }, /** @param {RuleContext} context */ diff --git a/lib/rules/syntaxes/define-options.js b/lib/rules/syntaxes/define-options.js new file mode 100644 index 000000000..133a38721 --- /dev/null +++ b/lib/rules/syntaxes/define-options.js @@ -0,0 +1,22 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../../utils/index') + +module.exports = { + supported: '>=3.3.0', + /** @param {RuleContext} context @returns {RuleListener} */ + createScriptVisitor(context) { + return utils.defineScriptSetupVisitor(context, { + onDefineOptionsEnter(node) { + context.report({ + node, + messageId: 'forbiddenDefineOptions' + }) + } + }) + } +} diff --git a/lib/rules/syntaxes/define-slots.js b/lib/rules/syntaxes/define-slots.js new file mode 100644 index 000000000..450ec3e89 --- /dev/null +++ b/lib/rules/syntaxes/define-slots.js @@ -0,0 +1,22 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../../utils/index') + +module.exports = { + supported: '>=3.3.0', + /** @param {RuleContext} context @returns {RuleListener} */ + createScriptVisitor(context) { + return utils.defineScriptSetupVisitor(context, { + onDefineSlotsEnter(node) { + context.report({ + node, + messageId: 'forbiddenDefineSlots' + }) + } + }) + } +} diff --git a/tests/lib/rules/no-restricted-component-options.js b/tests/lib/rules/no-restricted-component-options.js index 87ff4e41f..484f6700c 100644 --- a/tests/lib/rules/no-restricted-component-options.js +++ b/tests/lib/rules/no-restricted-component-options.js @@ -101,6 +101,11 @@ tester.run('no-restricted-component-options', rule, { `, options: [['foo', 'bar']] + }, + { + filename: 'test.vue', + code: ``, + options: ['Foo'] } ], invalid: [ @@ -304,6 +309,17 @@ tester.run('no-restricted-component-options', rule, { line: 5 } ] + }, + { + filename: 'test.vue', + code: ``, + options: ['Foo'], + errors: [ + { + message: 'Using `Foo` is not allowed.', + line: 1 + } + ] } ] }) diff --git a/tests/lib/rules/no-unsupported-features/define-options.js b/tests/lib/rules/no-unsupported-features/define-options.js new file mode 100644 index 000000000..7ee6277ae --- /dev/null +++ b/tests/lib/rules/no-unsupported-features/define-options.js @@ -0,0 +1,59 @@ +/** + * @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('define-options', '^3.2.0') +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019 + } +}) + +tester.run('no-unsupported-features/define-options', rule, { + valid: [ + { + code: ` + `, + options: buildOptions({ version: '^3.3.0' }) + }, + { + code: ` + `, + options: buildOptions() + }, + { + code: ` + `, + options: buildOptions({ version: '^3.0.0', ignores: ['define-options'] }) + } + ], + invalid: [ + { + code: ` + `, + options: buildOptions(), + errors: [ + { + message: + '`defineOptions()` macros are not supported until Vue.js "3.3.0".', + line: 3 + } + ] + } + ] +}) diff --git a/tests/lib/rules/no-unsupported-features/define-slots.js b/tests/lib/rules/no-unsupported-features/define-slots.js new file mode 100644 index 000000000..2fef117c7 --- /dev/null +++ b/tests/lib/rules/no-unsupported-features/define-slots.js @@ -0,0 +1,59 @@ +/** + * @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('define-slots', '^3.2.0') +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019 + } +}) + +tester.run('no-unsupported-features/define-slots', rule, { + valid: [ + { + code: ` + `, + options: buildOptions({ version: '^3.3.0' }) + }, + { + code: ` + `, + options: buildOptions() + }, + { + code: ` + `, + options: buildOptions({ version: '^3.0.0', ignores: ['define-slots'] }) + } + ], + invalid: [ + { + code: ` + `, + options: buildOptions(), + errors: [ + { + message: + '`defineSlots()` macros are not supported until Vue.js "3.3.0".', + line: 3 + } + ] + } + ] +}) From 7c36873585fe772b7828611870f123f388efb122 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 13 May 2023 14:26:25 +0900 Subject: [PATCH 2/4] make auto-fixable --- lib/rules/syntaxes/define-options.js | 54 ++++++++++++++++++- .../no-unsupported-features/define-options.js | 33 +++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/lib/rules/syntaxes/define-options.js b/lib/rules/syntaxes/define-options.js index 133a38721..0ef008ca7 100644 --- a/lib/rules/syntaxes/define-options.js +++ b/lib/rules/syntaxes/define-options.js @@ -10,13 +10,65 @@ module.exports = { supported: '>=3.3.0', /** @param {RuleContext} context @returns {RuleListener} */ createScriptVisitor(context) { + const sourceCode = context.getSourceCode() return utils.defineScriptSetupVisitor(context, { onDefineOptionsEnter(node) { context.report({ node, - messageId: 'forbiddenDefineOptions' + messageId: 'forbiddenDefineOptions', + fix(fixer) { + return fix(fixer, node) + } }) } }) + + /** + * @param {RuleFixer} fixer + * @param {CallExpression} node defineOptions() node + */ + function fix(fixer, node) { + if (node.arguments.length === 0) return null + const scriptSetup = utils.getScriptSetupElement(context) + if (!scriptSetup) return null + if ( + scriptSetup.parent.children + .filter(utils.isVElement) + .some( + (node) => + node.name === 'script' && !utils.hasAttribute(node, 'setup') + ) + ) { + // has `\n` + ), + fixer.removeRange(removeRange) + ] + } } } diff --git a/tests/lib/rules/no-unsupported-features/define-options.js b/tests/lib/rules/no-unsupported-features/define-options.js index 7ee6277ae..10b2069b2 100644 --- a/tests/lib/rules/no-unsupported-features/define-options.js +++ b/tests/lib/rules/no-unsupported-features/define-options.js @@ -12,7 +12,8 @@ const buildOptions = utils.optionsBuilder('define-options', '^3.2.0') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), parserOptions: { - ecmaVersion: 2019 + ecmaVersion: 2019, + sourceType: 'module' } }) @@ -44,9 +45,37 @@ tester.run('no-unsupported-features/define-options', rule, { { code: ` `, + options: buildOptions(), + output: ` + +`, + errors: [ + { + message: + '`defineOptions()` macros are not supported until Vue.js "3.3.0".', + line: 3 + } + ] + }, + { + code: ` + `, options: buildOptions(), + output: ` + +`, errors: [ { message: From 671132fb3b4ee0bab2434a141c03f0d7a683db61 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 13 May 2023 20:59:51 +0900 Subject: [PATCH 3/4] revert --- lib/rules/no-restricted-component-options.js | 14 +------------- .../lib/rules/no-restricted-component-options.js | 16 ---------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js index 5af68053c..fc90c6a8f 100644 --- a/lib/rules/no-restricted-component-options.js +++ b/lib/rules/no-restricted-component-options.js @@ -173,25 +173,13 @@ module.exports = { /** @type {ParsedOption[]} */ const options = context.options.map(parseOption) - return utils.compositingVisitors( - utils.defineVueVisitor(context, { + return utils.defineVueVisitor(context, { onVueObjectEnter(node) { for (const option of options) { verify(node, option.test, option.message) } } - }), - utils.defineScriptSetupVisitor(context, { - onDefineOptionsEnter(node) { - if (node.arguments.length === 0) return - const define = node.arguments[0] - if (define.type !== 'ObjectExpression') return - for (const option of options) { - verify(define, option.test, option.message) - } - } }) - ) /** * @param {ObjectExpression} node diff --git a/tests/lib/rules/no-restricted-component-options.js b/tests/lib/rules/no-restricted-component-options.js index 484f6700c..87ff4e41f 100644 --- a/tests/lib/rules/no-restricted-component-options.js +++ b/tests/lib/rules/no-restricted-component-options.js @@ -101,11 +101,6 @@ tester.run('no-restricted-component-options', rule, { `, options: [['foo', 'bar']] - }, - { - filename: 'test.vue', - code: ``, - options: ['Foo'] } ], invalid: [ @@ -309,17 +304,6 @@ tester.run('no-restricted-component-options', rule, { line: 5 } ] - }, - { - filename: 'test.vue', - code: ``, - options: ['Foo'], - errors: [ - { - message: 'Using `Foo` is not allowed.', - line: 1 - } - ] } ] }) From 2becc02ba29f635fbdc2348087610b70371401ba Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 13 May 2023 21:00:41 +0900 Subject: [PATCH 4/4] format --- lib/rules/no-restricted-component-options.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rules/no-restricted-component-options.js b/lib/rules/no-restricted-component-options.js index fc90c6a8f..f50f091d5 100644 --- a/lib/rules/no-restricted-component-options.js +++ b/lib/rules/no-restricted-component-options.js @@ -174,12 +174,12 @@ module.exports = { const options = context.options.map(parseOption) return utils.defineVueVisitor(context, { - onVueObjectEnter(node) { - for (const option of options) { - verify(node, option.test, option.message) - } + onVueObjectEnter(node) { + for (const option of options) { + verify(node, option.test, option.message) } - }) + } + }) /** * @param {ObjectExpression} node