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