123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /**
- * @fileoverview disallow mutation component props
- * @author 2018 Armano
- */
- 'use strict'
- /**
- * @typedef {{name?: string, set: Set<string>}} PropsInfo
- */
- const utils = require('../utils')
- const { findVariable } = require('@eslint-community/eslint-utils')
- // 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'
- ])
- /**
- * @param {ASTNode} node
- * @returns {VExpressionContainer}
- */
- function getVExpressionContainer(node) {
- let n = node
- while (n.type !== 'VExpressionContainer') {
- n = /** @type {ASTNode} */ (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) {
- // foo.id
- return false
- }
- } else if (
- parent.type === 'Property' &&
- parent.key === node &&
- !parent.computed
- ) {
- // {id: foo}
- return false
- }
- const exprContainer = getVExpressionContainer(node)
- for (const reference of exprContainer.references) {
- if (reference.variable != null) {
- // Not vm reference
- 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.'
- }
- },
- /** @param {RuleContext} context */
- create(context) {
- const { shallowOnly } = parseOptions(context.options[0])
- /** @type {Map<ObjectExpression|CallExpression, PropsInfo>} */
- const propsMap = 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
- const str = context.getSourceCode().getText(expr)
- return `[${str}]`
- }
- 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.getSourceCode().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) {
- // no arguments
- return
- }
- if (
- propsParam.type === 'RestElement' ||
- propsParam.type === 'ArrayPattern'
- ) {
- // cannot check
- return
- }
- for (const { node: prop, path } of iteratePatternProperties(
- propsParam,
- []
- )) {
- verifyPropVariable(prop, path)
- }
- },
- /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
- '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 &&
- /** @type {PropsInfo} */ (propsMap.get(vueNode)).set.has(name)
- ) {
- verifyMutating(mem, name)
- }
- }
- }),
- utils.defineTemplateBodyVisitor(context, {
- /** @param {ThisExpression & { parent: MemberExpression } } node */
- 'VExpressionContainer MemberExpression > ThisExpression'(node) {
- if (!vueObjectData) {
- return
- }
- const mem = node.parent
- if (mem.object !== node) {
- return
- }
- const name = utils.getStaticPropertyName(mem)
- if (
- name &&
- /** @type {PropsInfo} */ (
- propsMap.get(vueObjectData.object)
- ).set.has(name)
- ) {
- verifyMutating(mem, name)
- }
- },
- /** @param {Identifier } node */
- 'VExpressionContainer Identifier'(node) {
- if (!vueObjectData) {
- return
- }
- if (!isVmReference(node)) {
- return
- }
- const propsInfo = /** @type {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)
- }
- },
- /** @param {ESNode} node */
- "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 = /** @type {PropsInfo} */ (
- propsMap.get(vueObjectData.object)
- )
- const nodes = utils.getMemberChaining(node)
- const first = nodes[0]
- let name
- if (isVmReference(first)) {
- if (first.name === propsInfo.name) {
- // props variable
- 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)
- }
- })
- )
- }
- }
|