UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

355 lines (352 loc) 14 kB
'use strict'; const require_runtime = require('../_virtual/_rolldown/runtime.js'); const require_index = require('../utils/index.js'); //#region lib/rules/v-on-handler-style.js /** * @author Yosuke Ota <https://github.com/ota-meshi> * See LICENSE file in root directory for full license. */ var require_v_on_handler_style = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => { const utils = require_index.default; /** * @typedef {import('eslint').ReportDescriptorFix} ReportDescriptorFix * @typedef {'method' | 'inline' | 'inline-function'} HandlerKind * @typedef {object} ObjectOption * @property {boolean} [ignoreIncludesComment] */ /** * @param {RuleContext} context */ function parseOptions(context) { /** @type {[HandlerKind | HandlerKind[] | undefined, ObjectOption | undefined]} */ const options = context.options; /** @type {HandlerKind[]} */ const allows = []; if (options[0]) if (Array.isArray(options[0])) allows.push(...options[0]); else allows.push(options[0]); else allows.push("method", "inline-function"); return { allows, ignoreIncludesComment: !!(options[1] || {}).ignoreIncludesComment }; } /** * Check whether the given token is a quote. * @param {Token} token The token to check. * @returns {boolean} `true` if the token is a quote. */ function isQuote(token) { return token != null && token.type === "Punctuator" && (token.value === "\"" || token.value === "'"); } /** * Check whether the given node is an identifier call expression. e.g. `foo()` * @param {Expression} node The node to check. * @returns {node is CallExpression & {callee: Identifier}} */ function isIdentifierCallExpression(node) { if (node.type !== "CallExpression") return false; if (node.optional) return false; return node.callee.type === "Identifier"; } /** * Returns a call expression node if the given VOnExpression or BlockStatement consists * of only a single identifier call expression. * e.g. * @click="foo()" * @click="{ foo() }" * @click="foo();;" * @param {VOnExpression | BlockStatement} node * @returns {CallExpression & {callee: Identifier} | null} */ function getIdentifierCallExpression(node) { /** @type {ExpressionStatement} */ let exprStatement; let body = node.body; while (true) { const statements = body.filter((st) => st.type !== "EmptyStatement"); if (statements.length !== 1) return null; const statement = statements[0]; if (statement.type === "ExpressionStatement") { exprStatement = statement; break; } if (statement.type === "BlockStatement") { body = statement.body; continue; } return null; } const expression = exprStatement.expression; if (!isIdentifierCallExpression(expression)) return null; return expression; } module.exports = { meta: { type: "suggestion", docs: { description: "enforce writing style for handlers in `v-on` directives", categories: void 0, url: "https://eslint.vuejs.org/rules/v-on-handler-style.html" }, fixable: "code", schema: [{ oneOf: [{ enum: ["inline", "inline-function"] }, { type: "array", items: [{ const: "method" }, { enum: ["inline", "inline-function"] }], uniqueItems: true, additionalItems: false, minItems: 2, maxItems: 2 }] }, { type: "object", properties: { ignoreIncludesComment: { type: "boolean" } }, additionalProperties: false }], messages: { preferMethodOverInline: "Prefer method handler over inline handler in v-on.", preferMethodOverInlineWithoutIdCall: "Prefer method handler over inline handler in v-on. Note that you may need to create a new method.", preferMethodOverInlineFunction: "Prefer method handler over inline function in v-on.", preferMethodOverInlineFunctionWithoutIdCall: "Prefer method handler over inline function in v-on. Note that you may need to create a new method.", preferInlineOverMethod: "Prefer inline handler over method handler in v-on.", preferInlineOverInlineFunction: "Prefer inline handler over inline function in v-on.", preferInlineOverInlineFunctionWithMultipleParams: "Prefer inline handler over inline function in v-on. Note that the custom event must be changed to a single payload.", preferInlineFunctionOverMethod: "Prefer inline function over method handler in v-on.", preferInlineFunctionOverInline: "Prefer inline function over inline handler in v-on." } }, create(context) { const sourceCode = context.sourceCode; const { allows, ignoreIncludesComment } = parseOptions(context); /** @type {Set<VElement>} */ const upperElements = /* @__PURE__ */ new Set(); /** @type {Map<string, number>} */ const methodParamCountMap = /* @__PURE__ */ new Map(); /** @type {Identifier[]} */ const $eventIdentifiers = []; /** * Verify for inline handler. * @param {VOnExpression} node * @param {HandlerKind} kind * @returns {boolean} Returns `true` if reported. */ function verifyForInlineHandler(node, kind) { switch (kind) { case "method": return verifyCanUseMethodHandlerForInlineHandler(node); case "inline-function": reportCanUseInlineFunctionForInlineHandler(node); return true; } return false; } /** * Report for method handler. * @param {Identifier} node * @param {HandlerKind} kind * @returns {boolean} Returns `true` if reported. */ function reportForMethodHandler(node, kind) { switch (kind) { case "inline": case "inline-function": context.report({ node, messageId: kind === "inline" ? "preferInlineOverMethod" : "preferInlineFunctionOverMethod" }); return true; } return false; } /** * Verify for inline function handler. * @param {ArrowFunctionExpression | FunctionExpression} node * @param {HandlerKind} kind * @returns {boolean} Returns `true` if reported. */ function verifyForInlineFunction(node, kind) { switch (kind) { case "method": return verifyCanUseMethodHandlerForInlineFunction(node); case "inline": reportCanUseInlineHandlerForInlineFunction(node); return true; } return false; } /** * Get token information for the given VExpressionContainer node. * @param {VExpressionContainer} node */ function getVExpressionContainerTokenInfo(node) { const tokens = sourceCode.parserServices.getTemplateBodyTokenStore().getTokens(node, { includeComments: true }); const firstToken = tokens[0]; const lastToken = tokens.at(-1); if (!lastToken) return { rangeWithoutQuotes: [0, 0], hasComment: false, hasQuote: false }; const hasQuote = isQuote(firstToken); return { rangeWithoutQuotes: hasQuote ? [firstToken.range[1], lastToken.range[0]] : [firstToken.range[0], lastToken.range[1]], get hasComment() { return tokens.some((token) => token.type === "Block" || token.type === "Line"); }, hasQuote }; } /** * Checks whether the given node refers to a variable of the element. * @param {Expression | VOnExpression} node */ function hasReferenceUpperElementVariable(node) { for (const element of upperElements) for (const vv of element.variables) for (const reference of vv.references) { const { range } = reference.id; if (node.range[0] <= range[0] && range[1] <= node.range[1]) return true; } return false; } /** * Check if `v-on:click="foo()"` can be converted to `v-on:click="foo"` and report if it can. * @param {VOnExpression} node * @returns {boolean} Returns `true` if reported. */ function verifyCanUseMethodHandlerForInlineHandler(node) { const { rangeWithoutQuotes, hasComment } = getVExpressionContainerTokenInfo(node.parent); if (ignoreIncludesComment && hasComment) return false; const idCallExpr = getIdentifierCallExpression(node); if ((!idCallExpr || idCallExpr.arguments.length > 0) && hasReferenceUpperElementVariable(node)) return false; context.report({ node, messageId: idCallExpr ? "preferMethodOverInline" : "preferMethodOverInlineWithoutIdCall", fix: (fixer) => { if (hasComment || !idCallExpr || idCallExpr.arguments.length > 0) return null; const paramCount = methodParamCountMap.get(idCallExpr.callee.name); if (paramCount != null && paramCount > 0) return null; return fixer.replaceTextRange(rangeWithoutQuotes, sourceCode.getText(idCallExpr.callee)); } }); return true; } /** * Check if `v-on:click="() => foo()"` can be converted to `v-on:click="foo"` and report if it can. * @param {ArrowFunctionExpression | FunctionExpression} node * @returns {boolean} Returns `true` if reported. */ function verifyCanUseMethodHandlerForInlineFunction(node) { const { rangeWithoutQuotes, hasComment } = getVExpressionContainerTokenInfo(node.parent); if (ignoreIncludesComment && hasComment) return false; /** @type {CallExpression & {callee: Identifier} | null} */ let idCallExpr = null; if (node.body.type === "BlockStatement") idCallExpr = getIdentifierCallExpression(node.body); else if (isIdentifierCallExpression(node.body)) idCallExpr = node.body; if ((!idCallExpr || !isSameParamsAndArgs(idCallExpr)) && hasReferenceUpperElementVariable(node)) return false; context.report({ node, messageId: idCallExpr ? "preferMethodOverInlineFunction" : "preferMethodOverInlineFunctionWithoutIdCall", fix: (fixer) => { if (hasComment || !idCallExpr) return null; if (!isSameParamsAndArgs(idCallExpr)) return null; const paramCount = methodParamCountMap.get(idCallExpr.callee.name); if (paramCount != null && paramCount !== idCallExpr.arguments.length) return null; return fixer.replaceTextRange(rangeWithoutQuotes, sourceCode.getText(idCallExpr.callee)); } }); return true; /** * Checks whether parameters are passed as arguments as-is. * @param {CallExpression} expression */ function isSameParamsAndArgs(expression) { return node.params.length === expression.arguments.length && node.params.every((param, index) => { if (param.type !== "Identifier") return false; const arg = expression.arguments[index]; if (!arg || arg.type !== "Identifier") return false; return param.name === arg.name; }); } } /** * Report `v-on:click="foo()"` can be converted to `v-on:click="()=>foo()"`. * @param {VOnExpression} node * @returns {void} */ function reportCanUseInlineFunctionForInlineHandler(node) { context.report({ node, messageId: "preferInlineFunctionOverInline", *fix(fixer) { if ($eventIdentifiers.some(({ range }) => node.range[0] <= range[0] && range[1] <= node.range[1])) return; const { rangeWithoutQuotes, hasQuote } = getVExpressionContainerTokenInfo(node.parent); if (!hasQuote) return; yield fixer.insertTextBeforeRange(rangeWithoutQuotes, "() => "); const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore(); const firstToken = tokenStore.getFirstToken(node); const lastToken = tokenStore.getLastToken(node); if (firstToken.value === "{" && lastToken.value === "}") return; if (lastToken.value !== ";" && node.body.length === 1 && node.body[0].type === "ExpressionStatement") return; yield fixer.insertTextBefore(firstToken, "{"); yield fixer.insertTextAfter(lastToken, "}"); } }); } /** * Report `v-on:click="() => foo()"` can be converted to `v-on:click="foo()"`. * @param {ArrowFunctionExpression | FunctionExpression} node * @returns {void} */ function reportCanUseInlineHandlerForInlineFunction(node) { context.report({ node, messageId: node.params.length > 1 ? "preferInlineOverInlineFunctionWithMultipleParams" : "preferInlineOverInlineFunction", fix: node.params.length > 0 ? null : (fixer) => { let text = sourceCode.getText(node.body); if (node.body.type === "BlockStatement") text = text.slice(1, -1); return fixer.replaceText(node, text); } }); } return utils.defineTemplateBodyVisitor(context, { VElement(node) { upperElements.add(node); }, "VElement:exit"(node) { upperElements.delete(node); }, "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer.value:exit"(node) { const expression = node.expression; if (!expression) return; switch (expression.type) { case "VOnExpression": if (allows[0] === "inline") return; for (const allow of allows) if (verifyForInlineHandler(expression, allow)) return; break; case "Identifier": if (allows[0] === "method") return; for (const allow of allows) if (reportForMethodHandler(expression, allow)) return; break; case "ArrowFunctionExpression": case "FunctionExpression": if (allows[0] === "inline-function") return; for (const allow of allows) if (verifyForInlineFunction(expression, allow)) return; break; default: return; } }, ...allows.includes("inline-function") ? { "Identifier[name=\"$event\"]"(node) { $eventIdentifiers.push(node); } } : {} }, allows.includes("method") ? utils.defineVueVisitor(context, { onVueObjectEnter(node) { for (const method of utils.iterateProperties(node, new Set(["methods"]))) { if (method.type !== "object") continue; const value = method.property.value; if (value.type === "FunctionExpression" || value.type === "ArrowFunctionExpression") methodParamCountMap.set(method.name, value.params.some((p) => p.type === "RestElement") ? Number.POSITIVE_INFINITY : value.params.length); } } }) : {}); } }; })); //#endregion Object.defineProperty(exports, 'default', { enumerable: true, get: function () { return require_v_on_handler_style(); } });