diff --git a/lib/rules/no-mutating-props.js b/lib/rules/no-mutating-props.js index 70559026b..93855fd4e 100644 --- a/lib/rules/no-mutating-props.js +++ b/lib/rules/no-mutating-props.js @@ -11,6 +11,34 @@ const { findVariable } = require('eslint-utils') // Rule Definition // ------------------------------------------------------------------------------ +// https://github.com/vuejs/vue-next/blob/7c11c58faf8840ab97b6449c98da0296a60dddd8/packages/shared/src/globalsWhitelist.ts +const GLOBALS_WHITE_LISTED = new Set([ + 'Infinity', + 'undefined', + 'NaN', + 'isFinite', + 'isNaN', + 'parseFloat', + 'parseInt', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'Math', + 'Number', + 'Date', + 'Array', + 'Object', + 'Boolean', + 'String', + 'RegExp', + 'Map', + 'Set', + 'JSON', + 'Intl', + 'BigInt' +]) + module.exports = { meta: { type: 'suggestion', @@ -191,12 +219,43 @@ module.exports = { } } + function* extractDefineVariableNames() { + const globalScope = context.getSourceCode().scopeManager.globalScope + if (globalScope) { + for (const variable of globalScope.variables) { + if (variable.defs.length) { + yield variable.name + } + } + const moduleScope = globalScope.childScopes.find( + (scope) => scope.type === 'module' + ) + for (const variable of (moduleScope && moduleScope.variables) || []) { + if (variable.defs.length) { + yield variable.name + } + } + } + } + return utils.compositingVisitors( {}, utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { + const defineVariableNames = new Set(extractDefineVariableNames()) + const propsSet = new Set( - props.map((p) => p.propName).filter(utils.isDef) + props + .map((p) => p.propName) + .filter( + /** + * @returns {propName is string} + */ + (propName) => + utils.isDef(propName) && + !GLOBALS_WHITE_LISTED.has(propName) && + !defineVariableNames.has(propName) + ) ) propsMap.set(node, propsSet) vueObjectData = { @@ -337,12 +396,22 @@ module.exports = { } }, /** @param {ESNode} node */ - "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"( + "VAttribute[directive=true]:matches([key.name.name='model'], [key.name.name='bind']) VExpressionContainer > *"( node ) { if (!vueObjectData) { return } + let attr = node.parent + while (attr && attr.type !== 'VAttribute') { + attr = attr.parent + } + if (attr && attr.directive && attr.key.name.name === 'bind') { + if (!attr.key.modifiers.some((mod) => mod.name === 'sync')) { + return + } + } + const nodes = utils.getMemberChaining(node) const first = nodes[0] let name diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js index c0d6c2736..5c976baa2 100644 --- a/tests/lib/rules/no-mutating-props.js +++ b/tests/lib/rules/no-mutating-props.js @@ -331,6 +331,28 @@ ruleTester.run('no-mutating-props', rule, { } } ` + }, + + { + // script setup with shadow + filename: 'test.vue', + code: ` + + + ` } ], @@ -582,6 +604,42 @@ ruleTester.run('no-mutating-props', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unexpected mutation of "prop" prop.', + line: 4 + }, + { + message: 'Unexpected mutation of "prop" prop.', + line: 5 + }, + { + message: 'Unexpected mutation of "prop" prop.', + line: 10 + } + ] + }, // setup { @@ -817,6 +875,43 @@ ruleTester.run('no-mutating-props', rule, { line: 6 } ] + }, + + { + // script setup with shadow + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unexpected mutation of "bar" prop.', + line: 4 + }, + { + message: 'Unexpected mutation of "window" prop.', + line: 5 + }, + { + message: 'Unexpected mutation of "Infinity" prop.', + line: 6 + } + ] } ] })