diff --git a/lib/rules/no-dupe-keys.js b/lib/rules/no-dupe-keys.js index f959f4b66..b007631f4 100644 --- a/lib/rules/no-dupe-keys.js +++ b/lib/rules/no-dupe-keys.js @@ -16,6 +16,48 @@ const utils = require('../utils') /** @type {GroupName[]} */ const GROUP_NAMES = ['props', 'computed', 'data', 'methods', 'setup'] +/** + * Gets the props pattern node from given `defineProps()` node + * @param {CallExpression} node + * @returns {Pattern|null} + */ +function getPropsPattern(node) { + let target = node + if ( + target.parent && + target.parent.type === 'CallExpression' && + target.parent.arguments[0] === target && + target.parent.callee.type === 'Identifier' && + target.parent.callee.name === 'withDefaults' + ) { + target = target.parent + } + + if ( + !target.parent || + target.parent.type !== 'VariableDeclarator' || + target.parent.init !== target + ) { + return null + } + return target.parent.id +} + +/** + * Checks whether the initialization of the given variable declarator node contains one of the references. + * @param {VariableDeclarator} node + * @param {ESNode[]} references + */ +function isInsideInitializer(node, references) { + const init = node.init + if (!init) { + return false + } + return references.some( + (id) => init.range[0] <= id.range[0] && id.range[1] <= init.range[1] + ) +} + module.exports = { meta: { type: 'problem', @@ -63,12 +105,27 @@ module.exports = { }), utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { + const propsNode = getPropsPattern(node) + const propReferences = [ + ...(propsNode ? extractReferences(propsNode) : []), + node + ] + for (const prop of props) { if (!prop.propName) continue const variable = findVariable(context.getScope(), prop.propName) if (!variable || variable.defs.length === 0) continue + if ( + variable.defs.some((def) => { + if (def.type !== 'Variable') return false + return isInsideInitializer(def.node, propReferences) + }) + ) { + continue + } + context.report({ node: variable.defs[0].node, message: "Duplicated key '{{name}}'.", @@ -80,5 +137,32 @@ module.exports = { } }) ) + + /** + * Extracts references from the given node. + * @param {Pattern} node + * @returns {Identifier[]} References + */ + function extractReferences(node) { + if (node.type === 'Identifier') { + const variable = findVariable(context.getScope(), node) + if (!variable) { + return [] + } + return variable.references.map((ref) => ref.identifier) + } + if (node.type === 'ObjectPattern') { + return node.properties.flatMap((prop) => + extractReferences(prop.type === 'Property' ? prop.value : prop) + ) + } + if (node.type === 'AssignmentPattern') { + return extractReferences(node.left) + } + if (node.type === 'RestElement') { + return extractReferences(node.argument) + } + return [] + } } } diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js index e1294032f..29384f567 100644 --- a/tests/lib/rules/no-dupe-keys.js +++ b/tests/lib/rules/no-dupe-keys.js @@ -416,6 +416,86 @@ ruleTester.run('no-dupe-keys', rule, { `, parser: require.resolve('vue-eslint-parser'), parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + const {foo,bar} = defineProps(['foo', 'bar']) + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + const {foo=42,bar='abc'} = defineProps(['foo', 'bar']) + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + filename: 'test.vue', + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } } ], @@ -912,7 +992,7 @@ ruleTester.run('no-dupe-keys', rule, { + `, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: "Duplicated key 'bar'.", + line: 5 + } + ] } ] })