355 lines
14 KiB
JavaScript
355 lines
14 KiB
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();
|
|
}
|
|
}); |