UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

308 lines (305 loc) 11 kB
'use strict'; const require_runtime = require('../_virtual/_rolldown/runtime.js'); const require_index = require('../utils/index.js'); //#region lib/rules/no-mutating-props.js /** * @fileoverview disallow mutation component props * @author 2018 Armano */ var require_no_mutating_props = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => { /** * @typedef {{name?: string, set: Set<string>}} PropsInfo */ const utils = require_index.default; const { findVariable } = require("@eslint-community/eslint-utils"); 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" ]); /** * @param {ASTNode} node * @returns {VExpressionContainer} */ function getVExpressionContainer(node) { let n = node; while (n.type !== "VExpressionContainer") n = n.parent; return n; } /** * @param {ASTNode} node * @returns {node is Identifier} */ function isVmReference(node) { if (node.type !== "Identifier") return false; const parent = node.parent; if (parent.type === "MemberExpression") { if (parent.property === node) return false; } else if (parent.type === "Property" && parent.key === node && !parent.computed) return false; const exprContainer = getVExpressionContainer(node); for (const reference of exprContainer.references) { if (reference.variable != null) continue; if (reference.id === node) return true; } return false; } /** * @param { object } options * @param { boolean } options.shallowOnly Enables mutating the value of a prop but leaving the reference the same */ function parseOptions(options) { return Object.assign({ shallowOnly: false }, options); } module.exports = { meta: { type: "suggestion", docs: { description: "disallow mutation of component props", categories: ["vue3-essential", "vue2-essential"], url: "https://eslint.vuejs.org/rules/no-mutating-props.html" }, fixable: null, schema: [{ type: "object", properties: { shallowOnly: { type: "boolean" } }, additionalProperties: false }], messages: { unexpectedMutation: "Unexpected mutation of \"{{key}}\" prop." } }, create(context) { const { shallowOnly } = parseOptions(context.options[0]); /** @type {Map<ObjectExpression|CallExpression, PropsInfo>} */ const propsMap = /* @__PURE__ */ new Map(); /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */ let vueObjectData = null; /** * @param {ASTNode} node * @param {string} name */ function report(node, name) { context.report({ node, messageId: "unexpectedMutation", data: { key: name } }); } /** * @param {MemberExpression|AssignmentProperty} node * @returns {string} */ function getPropertyNameText(node) { const name = utils.getStaticPropertyName(node); if (name) return name; if (node.computed) { const expr = node.type === "Property" ? node.key : node.property; return `[${context.sourceCode.getText(expr)}]`; } return "?unknown?"; } /** * @param {MemberExpression|Identifier} props * @param {string} name * @param {boolean} isRootProps */ function verifyMutating(props, name, isRootProps = false) { const invalid = utils.findMutating(props); if (invalid && isShallowOnlyInvalid(invalid, isRootProps)) report(invalid.node, name); } /** * @param {Pattern} param * @param {string[]} path * @returns {Generator<{ node: Identifier, path: string[] }>} */ function* iteratePatternProperties(param, path) { if (!param) return; switch (param.type) { case "Identifier": yield { node: param, path }; break; case "RestElement": yield* iteratePatternProperties(param.argument, path); break; case "AssignmentPattern": yield* iteratePatternProperties(param.left, path); break; case "ObjectPattern": for (const prop of param.properties) if (prop.type === "Property") { const name = getPropertyNameText(prop); yield* iteratePatternProperties(prop.value, [...path, name]); } else if (prop.type === "RestElement") yield* iteratePatternProperties(prop.argument, path); break; case "ArrayPattern": for (let index = 0; index < param.elements.length; index++) { const element = param.elements[index]; if (element) yield* iteratePatternProperties(element, [...path, `${index}`]); } break; } } /** * @param {Identifier} prop * @param {string[]} path */ function verifyPropVariable(prop, path) { const variable = findVariable(utils.getScope(context, prop), 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 (!isShallowOnlyInvalid(invalid, path.length === 0)) continue; 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); } } function* extractDefineVariableNames() { const globalScope = context.sourceCode.scopeManager.globalScope; if (globalScope) { for (const variable of globalScope.variables) if (variable.defs.length > 0) yield variable.name; const moduleScope = globalScope.childScopes.find((scope) => scope.type === "module"); for (const variable of moduleScope && moduleScope.variables || []) if (variable.defs.length > 0) yield variable.name; } } /** * Is shallowOnly false or the prop reassigned * @param {Exclude<ReturnType<typeof utils.findMutating>, null>} invalid * @param {boolean} isRootProps * @return {boolean} */ function isShallowOnlyInvalid(invalid, isRootProps) { return !shallowOnly || invalid.pathNodes.length === (isRootProps ? 1 : 0) && ["assignment", "update"].includes(invalid.kind); } return utils.compositingVisitors({}, utils.defineScriptSetupVisitor(context, { onDefinePropsEnter(node, props) { const defineVariableNames = new Set(extractDefineVariableNames()); const propsInfo = { name: "", set: new Set(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, propsInfo); vueObjectData = { type: "setup", object: 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; for (const { node: prop, path } of iteratePatternProperties(target.parent.id, [])) { if (path.length === 0) propsInfo.name = prop.name; else propsInfo.set.add(prop.name); verifyPropVariable(prop, path); } } }), utils.defineVueVisitor(context, { onVueObjectEnter(node) { propsMap.set(node, { set: new Set(utils.getComponentPropsFromOptions(node).map((p) => p.propName).filter(utils.isDef)) }); }, onVueObjectExit(node, { type }) { if ((!vueObjectData || vueObjectData.type !== "export" && vueObjectData.type !== "setup") && type !== "instance") vueObjectData = { type, object: node }; }, onSetupFunctionEnter(node) { const propsParam = node.params[0]; if (!propsParam) return; if (propsParam.type === "RestElement" || propsParam.type === "ArrayPattern") return; for (const { node: prop, path } of iteratePatternProperties(propsParam, [])) verifyPropVariable(prop, path); }, "MemberExpression > :matches(Identifier, ThisExpression)"(node, { node: vueNode }) { if (!utils.isThis(node, context)) return; const mem = node.parent; if (mem.object !== node) return; const name = utils.getStaticPropertyName(mem); if (name && propsMap.get(vueNode).set.has(name)) verifyMutating(mem, name); } }), utils.defineTemplateBodyVisitor(context, { "VExpressionContainer MemberExpression > ThisExpression"(node) { if (!vueObjectData) return; const mem = node.parent; if (mem.object !== node) return; const name = utils.getStaticPropertyName(mem); if (name && propsMap.get(vueObjectData.object).set.has(name)) verifyMutating(mem, name); }, "VExpressionContainer Identifier"(node) { if (!vueObjectData) return; if (!isVmReference(node)) return; const propsInfo = propsMap.get(vueObjectData.object); const isRootProps = !!node.name && propsInfo.name === node.name; const parent = node.parent; const name = isRootProps && parent.type === "MemberExpression" && utils.getStaticPropertyName(parent) || node.name; if (name && (propsInfo.set.has(name) || isRootProps)) verifyMutating(node, name, isRootProps); }, "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" && !attr.key.modifiers.some((mod) => mod.name === "sync")) return; const propsInfo = propsMap.get(vueObjectData.object); const nodes = utils.getMemberChaining(node); const first = nodes[0]; let name; if (isVmReference(first)) if (first.name === propsInfo.name) { if (shallowOnly && nodes.length > 2) return; name = nodes[1] && getPropertyNameText(nodes[1]) || first.name; } else { if (shallowOnly && nodes.length > 1) return; name = first.name; if (!name || !propsInfo.set.has(name)) return; } else if (first.type === "ThisExpression") { if (shallowOnly && nodes.length > 2) return; const mem = nodes[1]; if (!mem) return; name = utils.getStaticPropertyName(mem); if (!name || !propsInfo.set.has(name)) return; } else return; report(node, name); } })); } }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_no_mutating_props(); } });