eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
308 lines (305 loc) • 11 kB
JavaScript
'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();
}
});