create()

in src/dispatch/static/dispatch/eslint-local-rules.js [44:549]


    create(context) {
      const template = context.parserServices.getTemplateBodyTokenStore()
      let observerNode
      let formNode
      let observerRefNodes = []
      const rulesToImport = new Set()
      const vvImportNodes = []
      const vvRuleImportNodes = []
      const validationProviderNodes = []
      let vueObjectNode
      let hasSetup = false
      const methodNodes = []

      return context.parserServices.defineTemplateBodyVisitor(
        {
          'VElement[parent.type!="VElement"]:exit'(node) {
            if (rulesToImport.size) {
              if (!vueObjectNode) {
                return context.report({
                  node: context.sourceCode.ast,
                  message: "unable to locate vue object",
                })
              }
              if (hasSetup) {
                return context.report({
                  node: vueObjectNode,
                  message: "component already has setup defined",
                })
              }
            }

            if (
              !(
                vvImportNodes.length ||
                vvRuleImportNodes.length ||
                observerNode ||
                validationProviderNodes.length ||
                rulesToImport.size
              )
            )
              return

            return context.report({
              node,
              message: "replace validation-observer with v-form",
              fix(fixer) {
                const fixes = []

                // remove vee-validate imports
                vvImportNodes.forEach((node) => {
                  fixes.push(fixer.remove(node))
                  context.getDeclaredVariables(node).forEach((variable) => {
                    variable.references.forEach((reference) => {
                      fixes.push(
                        fixer.removeRange([
                          context.sourceCode.getIndexFromLoc({
                            line: reference.identifier.parent.loc.start.line,
                            column: 0,
                          }) - 1,
                          context.sourceCode.getIndexFromLoc(reference.identifier.parent.loc.end) +
                            1,
                        ])
                      )
                    })
                  })
                })
                vvRuleImportNodes.forEach((node) => {
                  fixes.push(fixer.remove(node))
                })

                if (observerNode) {
                  const observerRef = observerNode.startTag.attributes.find((attr) => {
                    return attr.type === "VAttribute" && attr.key.name === "ref"
                  })
                  const observerSlot = observerNode.startTag.attributes.find((attr) => {
                    return (
                      attr.type === "VAttribute" &&
                      attr.key.type === "VDirectiveKey" &&
                      attr.key.name.name === "slot"
                    )
                  })
                  const otherAttrs = observerNode.startTag.attributes
                    .filter((attr) => {
                      return attr !== observerRef && attr !== observerSlot
                    })
                    .map((attr) => context.sourceCode.getText(attr))

                  let formSubmitHandlerName
                  let formSubmitHandlerNode
                  if (formNode) {
                    const submitListener = formNode.startTag.attributes.find((attr) => {
                      return (
                        attr.directive &&
                        attr.key.name.name === "on" &&
                        attr.key.argument.name === "submit"
                      )
                    })

                    if (submitListener) {
                      formSubmitHandlerName = submitListener.value.expression.name
                      if (formSubmitHandlerName) {
                        methodNodes.forEach((node) => {
                          if (node.key.name === formSubmitHandlerName) {
                            formSubmitHandlerNode = node
                          }
                        })
                        if (!formSubmitHandlerNode) {
                          throw new Error("Unable to locate form submit handler")
                        }
                      }
                    } else {
                      throw new Error("No submit listener")
                    }
                  }

                  let newStartTag = "<v-form"
                  if (observerRef) {
                    newStartTag += ` ref="form"`
                  }
                  if (otherAttrs.length) {
                    newStartTag += " " + otherAttrs.join(" ")
                  }
                  if (formNode) {
                    const formAttrs = formNode.startTag.attributes.map((attr) =>
                      context.sourceCode.getText(attr)
                    )
                    if (formAttrs.length) {
                      newStartTag += " " + formAttrs.join(" ")
                    }
                  } else {
                    newStartTag += ` @submit.prevent`
                  }
                  if (observerSlot) {
                    newStartTag += ` v-slot="{ isValid }"`
                  }
                  newStartTag += ">"

                  if (formNode) {
                    fixes.push(
                      fixer.replaceTextRange(
                        [observerNode.startTag.range[0], formNode.startTag.range[1]],
                        newStartTag
                      )
                    )
                  } else {
                    fixes.push(fixer.replaceText(observerNode.startTag, newStartTag))
                  }

                  if (observerSlot) {
                    observerNode.variables.forEach((variable) => {
                      if (variable.id.name === "invalid") {
                        variable.references.forEach((ref) => {
                          fixes.push(fixer.replaceText(ref.id, "!isValid.value"))
                        })
                      } else if (variable.id.name !== "validated") {
                        throw new Error("unsupported variable")
                      }
                    })
                  }

                  if (formNode) {
                    fixes.push(
                      fixer.replaceTextRange(
                        [formNode.endTag.range[0], observerNode.endTag.range[1]],
                        "</v-form>"
                      )
                    )
                  } else {
                    fixes.push(fixer.replaceText(observerNode.endTag, "</v-form>"))
                  }

                  observerRefNodes.forEach((node) => {
                    fixes.push(fixer.replaceText(node.property, "form"))
                    if (node.parent.property.name === "reset") {
                      fixes.push(fixer.replaceText(node.parent.property, "resetValidation"))
                    } else {
                      fixes.push({
                        range: Array(2).fill(
                          context.sourceCode.getIndexFromLoc({
                            line: node.loc.start.line + 1,
                            column: 0,
                          }) - 1
                        ),
                        text: " // TODO: find vuetify equivalent",
                      })
                    }
                  })

                  if (formSubmitHandlerNode) {
                    if (!formSubmitHandlerNode.value.async) {
                      fixes.push(fixer.insertTextBefore(formSubmitHandlerNode.key, "async "))
                    }
                    let paramName = "event"
                    if (!formSubmitHandlerNode.value.params.length) {
                      fixes.push(
                        fixer.replaceTextRange(
                          [
                            formSubmitHandlerNode.value.range[0],
                            formSubmitHandlerNode.value.body.range[0],
                          ],
                          `(${paramName}) `
                        )
                      )
                    } else {
                      paramName = formSubmitHandlerNode.value.params[0].name
                    }
                    const indent = context.sourceCode.lines
                      .at(formSubmitHandlerNode.value.body.body[0].loc.start.line - 1)
                      .match(/^\s*/)[0]
                    fixes.push(
                      fixer.insertTextBefore(
                        formSubmitHandlerNode.value.body.body[0],
                        `if (!(await ${paramName}).valid) return\n\n${indent}`
                      )
                    )
                  }
                }

                validationProviderNodes.forEach(({ node, child, rules, vid }) => {
                  fixes.push(fixer.removeRange([node.startTag.range[0], child.startTag.range[0]]))

                  if (node.variables.length) {
                    const b4 = template.getTokenBefore(
                      node.variables[0].references[0].id.parent.parent
                    )
                    fixes.push(
                      fixer.removeRange([
                        b4.range[1],
                        node.variables[0].references[0].id.parent.parent.range[1],
                      ])
                    )
                  } else {
                    node.children.forEach((child) => {
                      if (child.type === "VElement") {
                        child.startTag.attributes.forEach((attr) => {
                          if (
                            attr.key.type === "VDirectiveKey" &&
                            attr.key.name.name === "slot-scope"
                          ) {
                            fixes.push(fixer.remove(attr))
                            if (child.variables.length) {
                              child.variables.forEach((variable) => {
                                const b4 = template.getTokenBefore(
                                  variable.references[0].id.parent.parent
                                )
                                fixes.push(
                                  fixer.removeRange([
                                    b4.range[1],
                                    variable.references[0].id.parent.parent.range[1],
                                  ])
                                )
                              })
                            } else {
                              throw new Error("slot-scope without variables")
                            }
                          }
                        })
                      }
                    })
                  }

                  const isMultiline = child.startTag.loc.start.line !== child.startTag.loc.end.line
                  const indent = isMultiline
                    ? "\n" +
                      context.sourceCode.lines
                        .at(child.startTag.loc.start.line - 1)
                        .match(/^\s*/)[0] +
                      " ".repeat(2)
                    : " "

                  if (vid) {
                    fixes.push(
                      fixer.insertTextAfter(
                        child.startTag.attributes.at(-1),
                        indent +
                          (vid.directive
                            ? `:name=${context.sourceCode.getText(vid.value)}`
                            : `name="${vid.value.value}"`)
                      )
                    )
                  }
                  if (rules) {
                    let rulesArray
                    let rulesString
                    if (rules.directive) {
                      // dynamic rules
                      if (
                        rules.value.expression.type !== "TemplateLiteral" ||
                        rules.value.expression.quasis.length !== 2 ||
                        rules.value.expression.expressions.length !== 1 ||
                        rules.value.expression.expressions[0].type !== "ConditionalExpression" ||
                        rules.value.expression.expressions[0].alternate.type !== "Literal" ||
                        rules.value.expression.expressions[0].alternate.value !== ""
                      ) {
                        throw new Error("Unsupported dynamic rules")
                      }
                      const test = context.sourceCode.getText(
                        rules.value.expression.expressions[0].test
                      )
                      const rulesValue = rules.value.expression.expressions[0].consequent.value
                      rulesArray = rulesValue.split("|")
                      rulesString = `:rules="${test} ? [${rulesArray
                        .map((v) => `rules.${v}`)
                        .join(", ")}] : []"`
                    } else {
                      const rulesValue = rules.value.value
                      rulesArray = rulesValue.split("|")
                      rulesString = `:rules="[${rulesArray.map((v) => `rules.${v}`).join(", ")}]"`
                    }
                    fixes.push(
                      fixer.insertTextAfter(child.startTag.attributes.at(-1), indent + rulesString)
                    )
                    rulesArray.forEach((rule) => rulesToImport.add(rule))
                  }

                  fixes.push(
                    fixer.removeRange([
                      child.startTag.selfClosing ? child.startTag.range[1] : child.endTag.range[1],
                      node.endTag.range[1],
                    ])
                  )
                })

                if (rulesToImport.size) {
                  fixes.push(
                    fixer.insertTextBefore(
                      context.sourceCode.ast.body[0],
                      "import { " + [...rulesToImport].join(", ") + " } from '@/util/form'\n"
                    ),
                    fixer.insertTextAfterRange(
                      [vueObjectNode.range[0] + 1, vueObjectNode.range[0] + 1],
                      `
  setup() {
    return {
      rules: { ${[...rulesToImport].join(", ")} }
    }
  },`
                    )
                  )
                }

                return fixes
              },
            })
          },
          'VElement[rawName="ValidationObserver"]'(node) {
            if (observerNode) {
              throw new Error("multiple validation-observers")
            }
            observerNode = node
            formNode = node.children.find((child) => {
              return child.type === "VElement" && child.rawName === "form"
            })
          },
          'VElement[rawName="ValidationProvider"]'(node) {
            const children = node.children.filter((child) => {
              return child.type !== "VText" || child.value.trim()
            })
            const child = children[0]
            if (
              children.length !== 1 ||
              child.type !== "VElement" ||
              !(
                child.name.startsWith("v-") ||
                ["participant-select", "incident-select", "assignee-combobox"].includes(child.name)
              )
            ) {
              throw new Error(
                `validation-provider has unsupported children at ${node.loc.start.line}:${node.loc.start.column}`
              )
            }
            const vid = node.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                (attr.key.name === "vid" ||
                  (attr.directive &&
                    attr.key.name.name === "bind" &&
                    attr.key.argument?.name === "vid"))
              )
            })
            const name = node.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                (attr.key.name === "name" ||
                  (attr.directive &&
                    attr.key.name.name === "bind" &&
                    attr.key.argument?.name === "name"))
              )
            })
            const rules = node.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                (attr.key.name === "rules" ||
                  (attr.directive &&
                    attr.key.name.name === "bind" &&
                    attr.key.argument?.name === "rules"))
              )
            })
            const providerSlot = node.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                attr.key.type === "VDirectiveKey" &&
                attr.key.name.name === "slot"
              )
            })
            const providerOtherAttrs = node.startTag.attributes
              .filter((attr) => {
                return (
                  attr !== vid &&
                  attr !== name &&
                  attr !== rules &&
                  attr !== providerSlot &&
                  attr.key?.name !== "immediate"
                )
              })
              .map((attr) => context.sourceCode.getText(attr))
            const childName = child.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                (attr.key.name === "name" ||
                  (attr.directive &&
                    attr.key.name.name === "bind" &&
                    attr.key.argument?.name === "name"))
              )
            })
            const childRules = child.startTag.attributes.find((attr) => {
              return (
                attr.type === "VAttribute" &&
                (attr.key.name === "rules" ||
                  (attr.directive &&
                    attr.key.name.name === "bind" &&
                    attr.key.argument?.name === "rules"))
              )
            })

            if (providerOtherAttrs.length) {
              console.log("additional attributes", providerOtherAttrs)
              return context.report({
                node,
                message: "validation-provider has other attributes",
              })
            }

            if (providerSlot) {
              if (node.variables.length !== 1 || node.variables[0]?.id.name !== "errors") {
                return context.report({
                  node,
                  message: "validation-provider slot must only expose errors",
                })
              }
              if (
                node.variables[0].references.length !== 1 ||
                node.variables[0].references[0].id.parent.parent.type !== "VAttribute" ||
                node.variables[0].references[0].id.parent.parent.key.argument.name !==
                  "error-messages"
              ) {
                return context.report({
                  node: node.variables[0].references[0].id,
                  message: "validation-provider errors must only be used in error-messages",
                })
              }
            }

            if (childName) {
              return context.report({
                node: childName,
                message: "validation-provider child must not have a name",
              })
            }

            if (childRules) {
              return context.report({
                node: childRules,
                message: "validation-provider child should not have rules",
              })
            }

            validationProviderNodes.push({ node, child, rules, vid: vid || name })
          },
        },
        {
          'MemberExpression[object.type="MemberExpression"][object.property.name="$refs"][property.name="observer"]'(
            node
          ) {
            observerRefNodes.push(node)
          },
          'ImportDeclaration[source.value="vee-validate"]'(node) {
            vvImportNodes.push(node)
          },
          'ImportDeclaration[source.value="vee-validate/dist/rules"]'(node) {
            vvRuleImportNodes.push(node)
          },
          "ExportDefaultDeclaration > ObjectExpression"(node) {
            vueObjectNode = node
          },
          'ExportDefaultDeclaration > ObjectExpression > Property[key.name="setup"]'() {
            hasSetup = true
          },
          'ExportDefaultDeclaration > ObjectExpression > Property[key.name="methods"] > ObjectExpression > Property'(
            node
          ) {
            methodNodes.push(node)
          },
        }
      )
    },