From 7921d20863a46ecef9e4b850363d80600621fb76 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 17 Sep 2024 18:48:02 +0900 Subject: [PATCH] Add support for props destructure to `vue/no-restricted-props` rule --- lib/rules/no-restricted-props.js | 55 ++++++++++++----- lib/utils/index.js | 84 ++++++++++++++++++++++++++ tests/lib/rules/no-restricted-props.js | 52 +++++++++++++++- 3 files changed, 176 insertions(+), 15 deletions(-) diff --git a/lib/rules/no-restricted-props.js b/lib/rules/no-restricted-props.js index 21f53cc7d..2d2f74bb0 100644 --- a/lib/rules/no-restricted-props.js +++ b/lib/rules/no-restricted-props.js @@ -95,9 +95,9 @@ module.exports = { /** * @param {ComponentProp[]} props - * @param { { [key: string]: Property | undefined } } [withDefaultsProps] + * @param {(fixer: RuleFixer, propName: string, replaceKeyText: string) => Iterable} [fixPropInOtherPlaces] */ - function processProps(props, withDefaultsProps) { + function processProps(props, fixPropInOtherPlaces) { for (const prop of props) { if (!prop.propName) { continue @@ -118,7 +118,14 @@ module.exports = { : createSuggest( prop.key, option, - withDefaultsProps && withDefaultsProps[prop.propName] + fixPropInOtherPlaces + ? (fixer, replaceKeyText) => + fixPropInOtherPlaces( + fixer, + prop.propName, + replaceKeyText + ) + : undefined ) }) break @@ -129,7 +136,33 @@ module.exports = { return utils.compositingVisitors( utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { - processProps(props, utils.getWithDefaultsProps(node)) + processProps(props, fixPropInOtherPlaces) + + /** + * @param {RuleFixer} fixer + * @param {string} propName + * @param {string} replaceKeyText + */ + function fixPropInOtherPlaces(fixer, propName, replaceKeyText) { + /** @type {(Property|AssignmentProperty)[]} */ + const propertyNodes = [] + const withDefault = utils.getWithDefaultsProps(node)[propName] + if (withDefault) { + propertyNodes.push(withDefault) + } + const propDestructure = utils.getPropsDestructure(node)[propName] + if (propDestructure) { + propertyNodes.push(propDestructure) + } + return propertyNodes.map((propertyNode) => + propertyNode.shorthand + ? fixer.insertTextBefore( + propertyNode.value, + `${replaceKeyText}:` + ) + : fixer.replaceText(propertyNode.key, replaceKeyText) + ) + } } }), utils.defineVueVisitor(context, { @@ -144,10 +177,10 @@ module.exports = { /** * @param {Expression} node * @param {ParsedOption} option - * @param {Property} [withDefault] + * @param {(fixer: RuleFixer, replaceKeyText: string) => Iterable} [fixPropInOtherPlaces] * @returns {Rule.SuggestionReportDescriptor[]} */ -function createSuggest(node, option, withDefault) { +function createSuggest(node, option, fixPropInOtherPlaces) { if (!option.suggest) { return [] } @@ -168,14 +201,8 @@ function createSuggest(node, option, withDefault) { { fix(fixer) { const fixes = [fixer.replaceText(node, replaceText)] - if (withDefault) { - if (withDefault.shorthand) { - fixes.push( - fixer.insertTextBefore(withDefault.value, `${replaceText}:`) - ) - } else { - fixes.push(fixer.replaceText(withDefault.key, replaceText)) - } + if (fixPropInOtherPlaces) { + fixes.push(...fixPropInOtherPlaces(fixer, replaceText)) } return fixes.sort((a, b) => a.range[0] - b.range[0]) }, diff --git a/lib/utils/index.js b/lib/utils/index.js index 9671c00d4..c31f2d6af 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1537,6 +1537,28 @@ 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' + }, + /** + * Gets the props destructure property nodes for defineProp. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ + getPropsDestructure, getVueObjectType, /** @@ -3144,6 +3166,68 @@ function getWithDefaultsProps(node) { return result } +/** + * Gets the props destructure property nodes for defineProp. + * @param {CallExpression} node The node of defineProps + * @returns { Record } + */ +function getPropsDestructure(node) { + /** @type {ReturnType} */ + const result = Object.create(null) + const left = getLeftOfDefineProps(node) + if (!left || left.type !== 'ObjectPattern') { + return result + } + for (const prop of left.properties) { + if (prop.type !== 'Property') continue + const name = getStaticPropertyName(prop) + if (name != null) { + 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 + * @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-restricted-props.js b/tests/lib/rules/no-restricted-props.js index 2dc1bcf64..a91615e15 100644 --- a/tests/lib/rules/no-restricted-props.js +++ b/tests/lib/rules/no-restricted-props.js @@ -611,6 +611,56 @@ tester.run('no-restricted-props', rule, { } ] } - ]) + ]), + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 3, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'foo', suggest: 'Foo' }], + errors: [ + { + message: 'Using `foo` props is not allowed.', + line: 3, + suggestions: [ + { + desc: 'Instead, change to `Foo`.', + output: ` + + ` + } + ] + } + ] + } ] })