diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js index b8eeb17e4..c4c8bf236 100644 --- a/lib/rules/no-mutating-props.js +++ b/lib/rules/no-mutating-props.js @@ -26,9 +26,9 @@ module.exports = { }, /** @param {RuleContext} context */ create(context) { - /** @type {Map>} */ + /** @type {Map>} */ const propsMap = new Map() - /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | null } */ + /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */ let vueObjectData = null /** @@ -123,7 +123,7 @@ module.exports = { * @param {string[]} path * @returns {Generator<{ node: Identifier, path: string[] }>} */ - function* iterateParamProperties(param, path) { + function* iteratePatternProperties(param, path) { if (!param) { return } @@ -133,28 +133,94 @@ module.exports = { path } } else if (param.type === 'RestElement') { - yield* iterateParamProperties(param.argument, path) + yield* iteratePatternProperties(param.argument, path) } else if (param.type === 'AssignmentPattern') { - yield* iterateParamProperties(param.left, path) + yield* iteratePatternProperties(param.left, path) } else if (param.type === 'ObjectPattern') { for (const prop of param.properties) { if (prop.type === 'Property') { const name = getPropertyNameText(prop) - yield* iterateParamProperties(prop.value, [...path, name]) + yield* iteratePatternProperties(prop.value, [...path, name]) } else if (prop.type === 'RestElement') { - yield* iterateParamProperties(prop.argument, path) + yield* iteratePatternProperties(prop.argument, path) } } } else if (param.type === 'ArrayPattern') { for (let index = 0; index < param.elements.length; index++) { const element = param.elements[index] - yield* iterateParamProperties(element, [...path, `${index}`]) + yield* iteratePatternProperties(element, [...path, `${index}`]) } } } - return Object.assign( + /** + * @param {Identifier} prop + * @param {string[]} path + */ + function verifyPropVariable(prop, path) { + const variable = findVariable(context.getScope(), prop) + if (!variable) { + return + } + + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + const id = reference.identifier + + const invalid = utils.findMutating(id) + if (!invalid) { + continue + } + let name + if (path.length === 0) { + if (invalid.pathNodes.length === 0) { + continue + } + const mem = invalid.pathNodes[0] + name = getPropertyNameText(mem) + } else { + if (invalid.pathNodes.length === 0 && invalid.kind !== 'call') { + continue + } + name = path[0] + } + + report(invalid.node, name) + } + } + + return utils.compositingVisitors( {}, + utils.defineScriptSetupVisitor(context, { + onDefinePropsEnter(node, props) { + const propsSet = new Set( + props.map((p) => p.propName).filter(utils.isDef) + ) + propsMap.set(node, propsSet) + vueObjectData = { + type: 'setup', + object: node + } + + if ( + !node.parent || + node.parent.type !== 'VariableDeclarator' || + node.parent.init !== node + ) { + return + } + + for (const { node: prop, path } of iteratePatternProperties( + node.parent.id, + [] + )) { + verifyPropVariable(prop, path) + propsSet.add(prop.name) + } + } + }), utils.defineVueVisitor(context, { onVueObjectEnter(node) { propsMap.set( @@ -169,7 +235,9 @@ module.exports = { }, onVueObjectExit(node, { type }) { if ( - (!vueObjectData || vueObjectData.type !== 'export') && + (!vueObjectData || + (vueObjectData.type !== 'export' && + vueObjectData.type !== 'setup')) && type !== 'instance' ) { vueObjectData = { @@ -191,41 +259,11 @@ module.exports = { // cannot check return } - for (const { node: prop, path } of iterateParamProperties( + for (const { node: prop, path } of iteratePatternProperties( propsParam, [] )) { - const variable = findVariable(context.getScope(), prop) - if (!variable) { - continue - } - - for (const reference of variable.references) { - if (!reference.isRead()) { - continue - } - const id = reference.identifier - - const invalid = utils.findMutating(id) - if (!invalid) { - continue - } - let name - if (path.length === 0) { - if (invalid.pathNodes.length === 0) { - continue - } - const mem = invalid.pathNodes[0] - name = getPropertyNameText(mem) - } else { - if (invalid.pathNodes.length === 0 && invalid.kind !== 'call') { - continue - } - name = path[0] - } - - report(invalid.node, name) - } + verifyPropVariable(prop, path) } }, /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */ diff --git a/lib/utils/index.js b/lib/utils/index.js index bfe786aa3..a02bcd6ff 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -12,37 +12,8 @@ * @typedef {import('eslint').Rule.CodePathSegment} CodePathSegment */ /** - * @typedef {object} ComponentArrayPropDetectName - * @property {'array'} type - * @property {Literal | TemplateLiteral} key - * @property {string} propName - * @property {null} value - * @property {Expression | SpreadElement} node - * - * @typedef {object} ComponentArrayPropUnknownName - * @property {'array'} type - * @property {null} key - * @property {null} propName - * @property {null} value - * @property {Expression | SpreadElement} node - * - * @typedef {ComponentArrayPropDetectName | ComponentArrayPropUnknownName} ComponentArrayProp - * - * @typedef {object} ComponentObjectPropDetectName - * @property {'object'} type - * @property {Expression} key - * @property {string} propName - * @property {Expression} value - * @property {Property} node - * - * @typedef {object} ComponentObjectPropUnknownName - * @property {'object'} type - * @property {null} key - * @property {null} propName - * @property {Expression} value - * @property {Property} node - * - * @typedef {ComponentObjectPropDetectName | ComponentObjectPropUnknownName} ComponentObjectProp + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentArrayProp} ComponentArrayProp + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ComponentObjectProp} ComponentObjectProp */ /** * @typedef {object} ComponentArrayEmitDetectName @@ -90,6 +61,7 @@ * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueObjectType} VueObjectType * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueObjectData} VueObjectData * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').VueVisitor} VueVisitor + * @typedef {import('../../typings/eslint-plugin-vue/util-types/utils').ScriptSetupVisitor} ScriptSetupVisitor */ // ------------------------------------------------------------------------------ @@ -767,49 +739,7 @@ module.exports = { return [] } - if (propsNode.value.type === 'ObjectExpression') { - return propsNode.value.properties.filter(isProperty).map((prop) => { - const propName = getStaticPropertyName(prop) - if (propName != null) { - return { - type: 'object', - key: prop.key, - propName, - value: skipTSAsExpression(prop.value), - node: prop - } - } - return { - type: 'object', - key: null, - propName: null, - value: skipTSAsExpression(prop.value), - node: prop - } - }) - } else { - return propsNode.value.elements.filter(isDef).map((prop) => { - if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') { - const propName = getStringLiteralValue(prop) - if (propName != null) { - return { - type: 'array', - key: prop, - propName, - value: null, - node: prop - } - } - } - return { - type: 'array', - key: null, - propName: null, - value: null, - node: prop - } - }) - } + return getComponentPropsFromDefine(propsNode.value) }, /** @@ -975,23 +905,7 @@ module.exports = { * Checks whether the current file is uses ` ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + + ` } ], @@ -698,6 +725,65 @@ ruleTester.run('no-mutating-props', rule, { `, errors: ['Unexpected mutation of "[a]" prop.'] + }, + + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unexpected mutation of "value" prop.', + line: 3 + }, + { + message: 'Unexpected mutation of "props" prop.', + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Unexpected mutation of "value" prop.', + line: 6 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: 'Unexpected mutation of "value" prop.', + line: 6 + } + ] } ] }) diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index 6888e9a41..e1633eb5d 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -27,3 +27,60 @@ export interface VueVisitor extends VueVisitorBase { | ((node: VAST.ParamNode, obj: VueObjectData) => void) | undefined } + +type ScriptSetupVisitorBase = { + [T in keyof NodeListenerMap]?: (node: NodeListenerMap[T]) => void +} +export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { + onDefinePropsEnter?( + node: CallExpression, + props: (ComponentArrayProp | ComponentObjectProp)[] + ): void + onDefinePropsExit?( + node: CallExpression, + props: (ComponentArrayProp | ComponentObjectProp)[] + ): void + [query: string]: + | ((node: VAST.ParamNode) => void) + | (( + node: CallExpression, + props: (ComponentArrayProp | ComponentObjectProp)[] + ) => void) + | undefined +} + +type ComponentArrayPropDetectName = { + type: 'array' + key: Literal | TemplateLiteral + propName: string + value: null + node: Expression | SpreadElement +} +type ComponentArrayPropUnknownName = { + type: 'array' + key: null + propName: null + value: null + node: Expression | SpreadElement +} +export type ComponentArrayProp = + | ComponentArrayPropDetectName + | ComponentArrayPropUnknownName + +type ComponentObjectPropDetectName = { + type: 'object' + key: Expression + propName: string + value: Expression + node: Property +} +type ComponentObjectPropUnknownName = { + type: 'object' + key: null + propName: null + value: Expression + node: Property +} +export type ComponentObjectProp = + | ComponentObjectPropDetectName + | ComponentObjectPropUnknownName