113 lines
4.6 KiB
JavaScript
113 lines
4.6 KiB
JavaScript
const require_runtime = require('../_virtual/_rolldown/runtime.js');
|
|
const require_index = require('../utils/index.js');
|
|
const require_casing = require('../utils/casing.js');
|
|
|
|
//#region lib/rules/prefer-v-model.ts
|
|
/**
|
|
* @author Flo Edelmann
|
|
* See LICENSE file in root directory for full license.
|
|
*/
|
|
var import_utils = /* @__PURE__ */ require_runtime.__toESM(require_index.default);
|
|
/**
|
|
* Get the static argument name of a directive, or `null` for dynamic arguments.
|
|
*/
|
|
const getStaticArgName = (directive) => directive.key.argument?.type === "VIdentifier" ? directive.key.argument.rawName : null;
|
|
/**
|
|
* Extract the prop name from an `update:propName` event directive argument.
|
|
*/
|
|
function getUpdateEventPropName(onDirective) {
|
|
const argName = getStaticArgName(onDirective);
|
|
if (!argName?.startsWith("update:")) return null;
|
|
return argName.slice(7);
|
|
}
|
|
/**
|
|
* Check if the event handler is a simple mirror assignment of the bind expression.
|
|
* Matches: `bar = $event` or `(param) => bar = param`
|
|
*/
|
|
function isMirrorAssignment(bindExpr, onExpr, sourceCode) {
|
|
const bindText = sourceCode.getText(bindExpr);
|
|
if (onExpr.type === "VOnExpression") {
|
|
const statements = onExpr.body;
|
|
if (statements.length !== 1) return false;
|
|
const stmt = statements[0];
|
|
if (stmt.type !== "ExpressionStatement") return false;
|
|
const expr = stmt.expression;
|
|
if (expr.type !== "AssignmentExpression" || expr.operator !== "=") return false;
|
|
return sourceCode.getText(expr.left) === bindText && expr.right.type === "Identifier" && expr.right.name === "$event";
|
|
}
|
|
if (onExpr.type === "ArrowFunctionExpression") {
|
|
if (onExpr.params.length !== 1) return false;
|
|
const param = onExpr.params[0];
|
|
if (param.type !== "Identifier") return false;
|
|
const body = onExpr.body;
|
|
if (body.type !== "AssignmentExpression" || body.operator !== "=") return false;
|
|
return sourceCode.getText(body.left) === bindText && body.right.type === "Identifier" && body.right.name === param.name;
|
|
}
|
|
return false;
|
|
}
|
|
var prefer_v_model_default = {
|
|
meta: {
|
|
type: "suggestion",
|
|
docs: {
|
|
description: "enforce using `v-model` instead of `:prop`/`@update:prop` pair",
|
|
categories: void 0,
|
|
url: "https://eslint.vuejs.org/rules/prefer-v-model.html"
|
|
},
|
|
fixable: null,
|
|
hasSuggestions: true,
|
|
schema: [],
|
|
messages: {
|
|
preferVModel: "Prefer `{{ vModelName }}` over the `:{{ propName }}`/`@update:{{ eventName }}` pair.",
|
|
replaceWithVModel: "Replace with `{{ vModelName }}`."
|
|
}
|
|
},
|
|
create(context) {
|
|
const sourceCode = context.sourceCode;
|
|
return import_utils.default.defineTemplateBodyVisitor(context, { VStartTag(node) {
|
|
const element = node.parent;
|
|
if (!import_utils.default.isCustomComponent(element)) return;
|
|
const bindDirectives = [];
|
|
const onDirectives = [];
|
|
for (const attr of node.attributes) {
|
|
if (!attr.directive) continue;
|
|
if (attr.key.name.name === "bind" && getStaticArgName(attr) != null && attr.key.modifiers.length === 0) bindDirectives.push(attr);
|
|
if (attr.key.name.name === "on" && getUpdateEventPropName(attr) != null && attr.key.modifiers.length === 0) onDirectives.push(attr);
|
|
}
|
|
for (const bindDir of bindDirectives) {
|
|
const propName = getStaticArgName(bindDir);
|
|
if (!propName) continue;
|
|
const normalizedBindName = require_casing.camelCase(propName);
|
|
const matchingOnDir = onDirectives.find((onDir) => require_casing.camelCase(getUpdateEventPropName(onDir)) === normalizedBindName);
|
|
if (!matchingOnDir) continue;
|
|
const bindExpr = bindDir.value?.expression;
|
|
const onExpr = matchingOnDir.value?.expression;
|
|
if (!bindExpr || bindExpr.type === "VFilterSequenceExpression" || bindExpr.type === "VForExpression" || bindExpr.type === "VOnExpression" || bindExpr.type === "VSlotScopeExpression" || !onExpr || !isMirrorAssignment(bindExpr, onExpr, sourceCode)) continue;
|
|
const vModelName = normalizedBindName === "modelValue" ? "v-model" : `v-model:${propName}`;
|
|
const eventName = getUpdateEventPropName(matchingOnDir) ?? propName;
|
|
const vModelText = `${vModelName}=${sourceCode.getText(bindDir.value)}`;
|
|
context.report({
|
|
node: bindDir,
|
|
messageId: "preferVModel",
|
|
data: {
|
|
vModelName,
|
|
propName,
|
|
eventName
|
|
},
|
|
suggest: [{
|
|
messageId: "replaceWithVModel",
|
|
data: { vModelName },
|
|
*fix(fixer) {
|
|
yield fixer.replaceText(bindDir, vModelText);
|
|
const textBefore = sourceCode.getText().slice(0, matchingOnDir.range[0]);
|
|
const removeStart = matchingOnDir.range[0] - (textBefore.length - textBefore.trimEnd().length);
|
|
yield fixer.removeRange([removeStart, matchingOnDir.range[1]]);
|
|
}
|
|
}]
|
|
});
|
|
}
|
|
} });
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
exports.default = prefer_v_model_default; |