text: /**
   * @fileoverview disallow mutation component props
   * @author 2018 Armano
   */
  'use strict'
  
  const utils = require('../utils')
  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',
      docs: {
        description: 'disallow mutation of component props',
        categories: ['vue3-essential', 'essential'],
        url: 'https://eslint.vuejs.org/rules/no-mutating-props.html'
      },
      fixable: null, // or "code" or "whitespace"
      schema: [
        // fill in your schema
      ]
    },
    /** @param {RuleContext} context */
    create(context) {
      /** @type {Map<ObjectExpression|CallExpression, Set<string>>} */
      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,
          message: 'Unexpected mutation of "{{key}}" prop.',
          data: {
            key: name
          }
        })
      }
  
      /**
       * @param {ASTNode} node
       * @returns {VExpressionContainer}
       */
      function getVExpressionContainer(node) {
        let n = node
        while (n.type !== 'VExpressionContainer') {
          n = /** @type {ASTNode} */ (n.parent)
        }
        return n
      }
      /**
       * @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 {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') {
          // {id: foo}
          if (parent.key === node && !parent.computed) {
            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 {MemberExpression|Identifier} props
       * @param {string} name
       */
      function verifyMutating(props, name) {
        const invalid = utils.findMutating(props)
        if (invalid) {
          report(invalid.node, name)
        }
      }
  
      /**
       * @param {Pattern} param
       * @param {string[]} path
       * @returns {Generator<{ node: Identifier, path: string[] }>}
       */
      function* iteratePatternProperties(param, path) {
        if (!param) {
          return
        }
        if (param.type === 'Identifier') {
          yield {
            node: param,
            path
          }
        } else if (param.type === 'RestElement') {
          yield* iteratePatternProperties(param.argument, path)
        } else if (param.type === 'AssignmentPattern') {
          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* iteratePatternProperties(prop.value, [...path, name])
            } else if (prop.type === 'RestElement') {
              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* iteratePatternProperties(element, [...path, `${index}`])
          }
        }
      }
  
      /**
       * @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)
        }
      }
  
      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(
                  /**
                   * @returns {propName is string}
                   */
                  (propName) =>
                    utils.isDef(propName) &&
                    !GLOBALS_WHITE_LISTED.has(propName) &&
                    !defineVariableNames.has(propName)
                )
            )
            propsMap.set(node, propsSet)
            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,
              []
            )) {
              verifyPropVariable(prop, path)
              propsSet.add(prop.name)
            }
          }
        }),
        utils.defineVueVisitor(context, {
          onVueObjectEnter(node) {
            propsMap.set(
              node,
              new Set(
                utils
                  .getComponentProps(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 {Set<string>} */ (propsMap.get(vueNode)).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 {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
                name
              )
            ) {
              verifyMutating(mem, name)
            }
          },
          /** @param {Identifier } node */
          'VExpressionContainer Identifier'(node) {
            if (!vueObjectData) {
              return
            }
            if (!isVmReference(node)) {
              return
            }
            const name = node.name
            if (
              name &&
              /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
                name
              )
            ) {
              verifyMutating(node, name)
            }
          },
          /** @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') {
              if (!attr.key.modifiers.some((mod) => mod.name === 'sync')) {
                return
              }
            }
  
            const nodes = utils.getMemberChaining(node)
            const first = nodes[0]
            let name
            if (isVmReference(first)) {
              name = first.name
            } else if (first.type === 'ThisExpression') {
              const mem = nodes[1]
              if (!mem) {
                return
              }
              name = utils.getStaticPropertyName(mem)
            } else {
              return
            }
            if (
              name &&
              /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
                name
              )
            ) {
              report(node, name)
            }
          }
        })
      )
    }
  }
  