From 81b33f25642a4aa0cbecc7b116182dd0f747c612 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Sat, 14 Sep 2024 11:06:13 +0900 Subject: [PATCH 1/2] Add support for props destructure to `vue/no-boolean-default` rule --- lib/rules/no-boolean-default.js | 68 +++++++++++++++++---------- lib/utils/index.js | 63 +++++++++++++++++++++++++ tests/lib/rules/no-boolean-default.js | 50 +++++++++++++++++++- 3 files changed, 155 insertions(+), 26 deletions(-) diff --git a/lib/rules/no-boolean-default.js b/lib/rules/no-boolean-default.js index 6796c0329..2ff6a2d06 100644 --- a/lib/rules/no-boolean-default.js +++ b/lib/rules/no-boolean-default.js @@ -8,18 +8,27 @@ const utils = require('../utils') /** * @typedef {import('../utils').ComponentProp} ComponentProp + * @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp */ /** - * @param {Property | SpreadElement} prop + * @param {Expression|undefined} node + */ +function isBooleanIdentifier(node) { + return Boolean(node && node.type === 'Identifier' && node.name === 'Boolean') +} + +/** + * Detects whether given prop node is a Boolean + * @param {ComponentObjectProp} prop + * @return {Boolean} */ function isBooleanProp(prop) { + const value = utils.skipTSAsExpression(prop.value) return ( - prop.type === 'Property' && - prop.key.type === 'Identifier' && - prop.key.name === 'type' && - prop.value.type === 'Identifier' && - prop.value.name === 'Boolean' + isBooleanIdentifier(value) || + (value.type === 'ObjectExpression' && + isBooleanIdentifier(utils.findProperty(value, 'type')?.value)) ) } @@ -55,40 +64,40 @@ module.exports = { const booleanType = context.options[0] || 'no-default' /** * @param {ComponentProp} prop - * @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions] + * @param {(propName: string) => Expression[]} otherDefaultProvider */ - function processProp(prop, withDefaultsExpressions) { + function processProp(prop, otherDefaultProvider) { if (prop.type === 'object') { - if (prop.value.type !== 'ObjectExpression') { + if (!isBooleanProp(prop)) { return } - if (!prop.value.properties.some(isBooleanProp)) { - return + if (prop.value.type === 'ObjectExpression') { + const defaultNode = getDefaultNode(prop.value) + if (defaultNode) { + verifyDefaultExpression(defaultNode.value) + } } - const defaultNode = getDefaultNode(prop.value) - if (!defaultNode) { - return + if (prop.propName != null) { + for (const defaultNode of otherDefaultProvider(prop.propName)) { + verifyDefaultExpression(defaultNode) + } } - verifyDefaultExpression(defaultNode.value) } else if (prop.type === 'type') { if (prop.types.length !== 1 || prop.types[0] !== 'Boolean') { return } - const defaultNode = - withDefaultsExpressions && withDefaultsExpressions[prop.propName] - if (!defaultNode) { - return + for (const defaultNode of otherDefaultProvider(prop.propName)) { + verifyDefaultExpression(defaultNode) } - verifyDefaultExpression(defaultNode) } } /** * @param {ComponentProp[]} props - * @param { { [key: string]: Expression | undefined } } [withDefaultsExpressions] + * @param {(propName: string) => Expression[]} otherDefaultProvider */ - function processProps(props, withDefaultsExpressions) { + function processProps(props, otherDefaultProvider) { for (const prop of props) { - processProp(prop, withDefaultsExpressions) + processProp(prop, otherDefaultProvider) } } @@ -118,11 +127,20 @@ module.exports = { } return utils.compositingVisitors( utils.executeOnVueComponent(context, (obj) => { - processProps(utils.getComponentPropsFromOptions(obj)) + processProps(utils.getComponentPropsFromOptions(obj), () => []) }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { - processProps(props, utils.getWithDefaultsPropExpressions(node)) + const defaultsByWithDefaults = + utils.getWithDefaultsPropExpressions(node) + const defaultsByAssignmentPatterns = + utils.getDefaultPropExpressionsForPropsDestructure(node) + processProps(props, (propName) => + [ + defaultsByWithDefaults[propName], + defaultsByAssignmentPatterns[propName]?.expression + ].filter(utils.isDef) + ) } }) ) diff --git a/lib/utils/index.js b/lib/utils/index.js index 9671c00d4..467a0b486 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1537,6 +1537,22 @@ module.exports = { * @returns { { [key: string]: Property | undefined } } */ getWithDefaultsProps, + /** + * Gets the default definition nodes for defineProp + * using the props destructure with assignment pattern. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ + getDefaultPropExpressionsForPropsDestructure, + /** + * Checks whether the given defineProps node is using Props Destructure. + * @param {CallExpression} node The node of defineProps + * @returns {boolean} + */ + isUsingPropsDestructure(node) { + const left = getLeftOfDefineProps(node) + return left?.type === 'ObjectPattern' + }, getVueObjectType, /** @@ -3144,6 +3160,53 @@ function getWithDefaultsProps(node) { return result } +/** + * Gets the default definition nodes for defineProp + * using the props destructure with assignment pattern. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ +function getDefaultPropExpressionsForPropsDestructure(node) { + const left = getLeftOfDefineProps(node) + if (!left || left.type !== 'ObjectPattern') { + return {} + } + /** @type {ReturnType} */ + const result = Object.create(null) + for (const prop of left.properties) { + if (prop.type !== 'Property') continue + const value = prop.value + if (value.type !== 'AssignmentPattern') continue + const name = getStaticPropertyName(prop) + if (name != null) { + result[name] = { prop, expression: value.right } + } + } + return result +} + +/** + * Gets the pattern of the left operand of defineProps. + * @param {CallExpression} node The node of defineProps + * @returns {Pattern | null} The pattern of the left operand of defineProps + */ +function getLeftOfDefineProps(node) { + let target = node + if (hasWithDefaults(target)) { + target = target.parent + } + if (!target.parent) { + return null + } + if ( + target.parent.type === 'VariableDeclarator' && + target.parent.init === target + ) { + return target.parent.id + } + return null +} + /** * Get all props from component options object. * @param {ObjectExpression} componentObject Object with component definition diff --git a/tests/lib/rules/no-boolean-default.js b/tests/lib/rules/no-boolean-default.js index 6f275c671..98524760c 100644 --- a/tests/lib/rules/no-boolean-default.js +++ b/tests/lib/rules/no-boolean-default.js @@ -326,6 +326,18 @@ ruleTester.run('no-boolean-default', rule, { parser: require.resolve('@typescript-eslint/parser') } } + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['default-false'], + languageOptions: { + parser: require('vue-eslint-parser') + } } ], @@ -512,6 +524,42 @@ ruleTester.run('no-boolean-default', rule, { } ] } - ]) + ]), + { + filename: 'test.vue', + code: ` + + `, + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: + 'Boolean prop should not set a default (Vue defaults it to false).', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['default-false'], + languageOptions: { + parser: require('vue-eslint-parser') + }, + errors: [ + { + message: 'Boolean prop should only be defaulted to false.', + line: 3 + } + ] + } ] }) From b5d17125b93b17b7ee48aa0a89e58536a010334c Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Tue, 17 Sep 2024 18:07:30 +0900 Subject: [PATCH 2/2] Update index.js --- lib/utils/index.js | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/utils/index.js b/lib/utils/index.js index 467a0b486..c31f2d6af 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1553,6 +1553,12 @@ module.exports = { const left = getLeftOfDefineProps(node) return left?.type === 'ObjectPattern' }, + /** + * Gets the props destructure property nodes for defineProp. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ + getPropsDestructure, getVueObjectType, /** @@ -3161,30 +3167,45 @@ function getWithDefaultsProps(node) { } /** - * Gets the default definition nodes for defineProp - * using the props destructure with assignment pattern. + * Gets the props destructure property nodes for defineProp. * @param {CallExpression} node The node of defineProps - * @returns { Record } + * @returns { Record } */ -function getDefaultPropExpressionsForPropsDestructure(node) { +function getPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) const left = getLeftOfDefineProps(node) if (!left || left.type !== 'ObjectPattern') { - return {} + return result } - /** @type {ReturnType} */ - const result = Object.create(null) for (const prop of left.properties) { if (prop.type !== 'Property') continue - const value = prop.value - if (value.type !== 'AssignmentPattern') continue const name = getStaticPropertyName(prop) if (name != null) { - result[name] = { prop, expression: value.right } + result[name] = prop } } return result } +/** + * Gets the default definition nodes for defineProp + * using the props destructure with assignment pattern. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ +function getDefaultPropExpressionsForPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) + for (const [name, prop] of Object.entries(getPropsDestructure(node))) { + if (!prop) continue + const value = prop.value + if (value.type !== 'AssignmentPattern') continue + result[name] = { prop, expression: value.right } + } + return result +} + /** * Gets the pattern of the left operand of defineProps. * @param {CallExpression} node The node of defineProps