eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
366 lines (363 loc) • 14.2 kB
JavaScript
;
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.js');
const require_index = require('../utils/index.js');
const require_casing$1 = require('../utils/casing.js');
//#region lib/rules/require-explicit-emits.js
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
var require_require_explicit_emits = /* @__PURE__ */ require_rolldown_runtime.__commonJSMin(((exports, module) => {
/**
* @typedef {import('../utils').ComponentEmit} ComponentEmit
* @typedef {import('../utils').ComponentProp} ComponentProp
* @typedef {import('../utils').VueObjectData} VueObjectData
*/
const { findVariable, isOpeningBraceToken, isClosingBraceToken, isOpeningBracketToken } = require("@eslint-community/eslint-utils");
const utils = require_index.default;
const { capitalize } = require_casing$1.default;
const FIX_EMITS_AFTER_OPTIONS = new Set([
"setup",
"data",
"computed",
"watch",
"methods",
"template",
"render",
"renderError",
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"activated",
"deactivated",
"beforeUnmount",
"unmounted",
"beforeDestroy",
"destroyed",
"renderTracked",
"renderTriggered",
"errorCaptured"
]);
/**
* @typedef {object} NameWithLoc
* @property {string} name
* @property {SourceLocation} loc
* @property {Range} range
*/
/**
* Get the name param node from the given CallExpression
* @param {CallExpression} node CallExpression
* @returns { NameWithLoc | null }
*/
function getNameParamNode(node) {
const nameLiteralNode = node.arguments[0];
if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
const name = utils.getStringLiteralValue(nameLiteralNode);
if (name != null) return {
name,
loc: nameLiteralNode.loc,
range: nameLiteralNode.range
};
}
return null;
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "require `emits` option with name triggered by `$emit()`",
categories: ["vue3-strongly-recommended"],
url: "https://eslint.vuejs.org/rules/require-explicit-emits.html"
},
fixable: null,
hasSuggestions: true,
schema: [{
type: "object",
properties: { allowProps: { type: "boolean" } },
additionalProperties: false
}],
messages: {
missing: "The \"{{name}}\" event has been triggered but not declared on {{emitsKind}}.",
addOneOption: "Add the \"{{name}}\" to {{emitsKind}}.",
addArrayEmitsOption: "Add the {{emitsKind}} with array syntax and define \"{{name}}\" event.",
addObjectEmitsOption: "Add the {{emitsKind}} with object syntax and define \"{{name}}\" event."
}
},
create(context) {
const allowProps = !!(context.options[0] || {}).allowProps;
/** @type {Map<ObjectExpression | Program, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }>} */
const setupContexts = /* @__PURE__ */ new Map();
/** @type {Map<ObjectExpression | Program, ComponentEmit[]>} */
const vueEmitsDeclarations = /* @__PURE__ */ new Map();
/** @type {Map<ObjectExpression | Program, ComponentProp[]>} */
const vuePropsDeclarations = /* @__PURE__ */ new Map();
let emitParamName = "";
/**
* @typedef {object} VueTemplateDefineData
* @property {'export' | 'mark' | 'definition' | 'setup'} type
* @property {ObjectExpression | Program} define
* @property {ComponentEmit[]} emits
* @property {ComponentProp[]} props
* @property {CallExpression} [defineEmits]
*/
/** @type {VueTemplateDefineData | null} */
let vueTemplateDefineData = null;
/**
* @param {ComponentEmit[]} emits
* @param {ComponentProp[]} props
* @param {NameWithLoc} nameWithLoc
* @param {ObjectExpression | Program} vueDefineNode
*/
function verifyEmit(emits, props, nameWithLoc, vueDefineNode) {
const name = nameWithLoc.name;
if (emits.some((e) => e.emitName === name || e.emitName == null)) return;
if (allowProps) {
const key = `on${capitalize(name)}`;
if (props.some((e) => e.propName === key || e.propName == null)) return;
}
context.report({
loc: nameWithLoc.loc,
messageId: "missing",
data: {
name,
emitsKind: vueDefineNode.type === "ObjectExpression" ? "`emits` option" : "`defineEmits`"
},
suggest: buildSuggest(vueDefineNode, emits, nameWithLoc, context)
});
}
const programNode = context.sourceCode.ast;
if (utils.isScriptSetup(context)) vueTemplateDefineData = {
type: "setup",
define: programNode,
emits: [],
props: []
};
const callVisitor = { CallExpression(node, info) {
const callee = utils.skipChainExpression(node.callee);
const nameWithLoc = getNameParamNode(node);
if (!nameWithLoc) return;
const vueDefineNode = info ? info.node : programNode;
const emitsDeclarations = vueEmitsDeclarations.get(vueDefineNode);
if (!emitsDeclarations) return;
let emit;
if (callee.type === "MemberExpression") {
const name = utils.getStaticPropertyName(callee);
if (name === "emit" || name === "$emit") emit = {
name,
member: callee
};
}
const setupContext = setupContexts.get(vueDefineNode);
if (setupContext) {
const { contextReferenceIds, emitReferenceIds } = setupContext;
if (callee.type === "Identifier" && emitReferenceIds.has(callee)) verifyEmit(emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], nameWithLoc, vueDefineNode);
else if (emit && emit.name === "emit") {
const memObject = utils.skipChainExpression(emit.member.object);
if (memObject.type === "Identifier" && contextReferenceIds.has(memObject)) verifyEmit(emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], nameWithLoc, vueDefineNode);
}
}
if (emit && emit.name === "$emit") {
const memObject = utils.skipChainExpression(emit.member.object);
if (utils.isThis(memObject, context)) verifyEmit(emitsDeclarations, vuePropsDeclarations.get(vueDefineNode) || [], nameWithLoc, vueDefineNode);
}
} };
return utils.defineTemplateBodyVisitor(context, { CallExpression(node) {
const callee = utils.skipChainExpression(node.callee);
const nameWithLoc = getNameParamNode(node);
if (!nameWithLoc) return;
if (!vueTemplateDefineData) return;
if (callee.type === "Identifier" && (callee.name === "$emit" || callee.name === emitParamName)) verifyEmit(vueTemplateDefineData.emits, vueTemplateDefineData.props, nameWithLoc, vueTemplateDefineData.define);
} }, utils.compositingVisitors(utils.defineScriptSetupVisitor(context, {
onDefineEmitsEnter(node, emits) {
vueEmitsDeclarations.set(programNode, emits);
if (vueTemplateDefineData && vueTemplateDefineData.type === "setup") {
vueTemplateDefineData.emits = emits;
vueTemplateDefineData.defineEmits = node;
}
if (!node.parent || node.parent.type !== "VariableDeclarator" || node.parent.init !== node) return;
const emitParam = node.parent.id;
if (emitParam.type !== "Identifier") return;
emitParamName = emitParam.name;
const variable = findVariable(utils.getScope(context, emitParam), emitParam);
if (!variable) return;
/** @type {Set<Identifier>} */
const emitReferenceIds = /* @__PURE__ */ new Set();
for (const reference of variable.references) {
if (!reference.isRead()) continue;
emitReferenceIds.add(reference.identifier);
}
setupContexts.set(programNode, {
contextReferenceIds: /* @__PURE__ */ new Set(),
emitReferenceIds
});
},
onDefinePropsEnter(_node, props) {
if (allowProps) {
vuePropsDeclarations.set(programNode, props);
if (vueTemplateDefineData && vueTemplateDefineData.type === "setup") vueTemplateDefineData.props = props;
}
},
...callVisitor
}), utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
vueEmitsDeclarations.set(node, utils.getComponentEmitsFromOptions(node));
if (allowProps) vuePropsDeclarations.set(node, utils.getComponentPropsFromOptions(node));
},
onSetupFunctionEnter(node, { node: vueNode }) {
const contextParam = node.params[1];
if (!contextParam) return;
if (contextParam.type === "RestElement") return;
if (contextParam.type === "ArrayPattern") return;
/** @type {Set<Identifier>} */
const contextReferenceIds = /* @__PURE__ */ new Set();
/** @type {Set<Identifier>} */
const emitReferenceIds = /* @__PURE__ */ new Set();
if (contextParam.type === "ObjectPattern") {
const emitProperty = utils.findAssignmentProperty(contextParam, "emit");
if (!emitProperty) return;
const emitParam = emitProperty.value;
const variable = emitParam.type === "Identifier" ? findVariable(utils.getScope(context, emitParam), emitParam) : null;
if (!variable) return;
for (const reference of variable.references) {
if (!reference.isRead()) continue;
emitReferenceIds.add(reference.identifier);
}
} else if (contextParam.type === "Identifier") {
const variable = findVariable(utils.getScope(context, contextParam), contextParam);
if (!variable) return;
for (const reference of variable.references) {
if (!reference.isRead()) continue;
contextReferenceIds.add(reference.identifier);
}
}
setupContexts.set(vueNode, {
contextReferenceIds,
emitReferenceIds
});
},
...callVisitor,
onVueObjectExit(node, { type }) {
const emits = vueEmitsDeclarations.get(node);
if ((!vueTemplateDefineData || vueTemplateDefineData.type !== "export" && vueTemplateDefineData.type !== "setup") && emits && (type === "mark" || type === "export" || type === "definition")) vueTemplateDefineData = {
type,
define: node,
emits,
props: vuePropsDeclarations.get(node) || []
};
setupContexts.delete(node);
vueEmitsDeclarations.delete(node);
vuePropsDeclarations.delete(node);
}
})));
}
};
/**
* @param {ObjectExpression|Program} define
* @param {ComponentEmit[]} emits
* @param {NameWithLoc} nameWithLoc
* @param {RuleContext} context
* @returns {Rule.SuggestionReportDescriptor[]}
*/
function buildSuggest(define, emits, nameWithLoc, context) {
const emitsKind = define.type === "ObjectExpression" ? "`emits` option" : "`defineEmits`";
const lastEmit = emits.filter(
/** @returns {e is ComponentEmit & {type:'array'|'object'}} */
(e) => e.type === "array" || e.type === "object"
).at(-1);
if (lastEmit) return [{
messageId: "addOneOption",
data: {
name: nameWithLoc.name,
emitsKind
},
fix(fixer) {
if (lastEmit.type === "array") return fixer.insertTextAfter(lastEmit.node, `, '${nameWithLoc.name}'`);
else if (lastEmit.type === "object") return fixer.insertTextAfter(lastEmit.node, `, '${nameWithLoc.name}': null`);
else return null;
}
}];
if (define.type !== "ObjectExpression") return [];
const object = define;
const propertyNodes = object.properties.filter(utils.isProperty);
const emitsOption = propertyNodes.find((p) => utils.getStaticPropertyName(p) === "emits");
if (emitsOption) {
const sourceCode$1 = context.sourceCode;
const emitsOptionValue = emitsOption.value;
if (emitsOptionValue.type === "ArrayExpression") {
const leftBracket = sourceCode$1.getFirstToken(emitsOptionValue, isOpeningBracketToken);
return [{
messageId: "addOneOption",
data: {
name: `${nameWithLoc.name}`,
emitsKind
},
fix(fixer) {
return fixer.insertTextAfter(leftBracket, `'${nameWithLoc.name}'${emitsOptionValue.elements.length > 0 ? "," : ""}`);
}
}];
} else if (emitsOptionValue.type === "ObjectExpression") {
const leftBrace = sourceCode$1.getFirstToken(emitsOptionValue, isOpeningBraceToken);
return [{
messageId: "addOneOption",
data: {
name: `${nameWithLoc.name}`,
emitsKind
},
fix(fixer) {
return fixer.insertTextAfter(leftBrace, `'${nameWithLoc.name}': null${emitsOptionValue.properties.length > 0 ? "," : ""}`);
}
}];
}
return [];
}
const sourceCode = context.sourceCode;
const afterOptionNode = propertyNodes.find((p) => FIX_EMITS_AFTER_OPTIONS.has(utils.getStaticPropertyName(p) || ""));
return [{
messageId: "addArrayEmitsOption",
data: {
name: `${nameWithLoc.name}`,
emitsKind
},
fix(fixer) {
if (afterOptionNode) return fixer.insertTextAfter(sourceCode.getTokenBefore(afterOptionNode), `\nemits: ['${nameWithLoc.name}'],`);
const lastPropertyNode = object.properties.at(-1);
if (lastPropertyNode) {
const before = propertyNodes.at(-1) || lastPropertyNode;
return fixer.insertTextAfter(before, `,\nemits: ['${nameWithLoc.name}']`);
} else {
const objectLeftBrace = sourceCode.getFirstToken(object, isOpeningBraceToken);
const objectRightBrace = sourceCode.getLastToken(object, isClosingBraceToken);
return fixer.insertTextAfter(objectLeftBrace, `\nemits: ['${nameWithLoc.name}']${objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? "" : "\n"}`);
}
}
}, {
messageId: "addObjectEmitsOption",
data: {
name: `${nameWithLoc.name}`,
emitsKind
},
fix(fixer) {
if (afterOptionNode) return fixer.insertTextAfter(sourceCode.getTokenBefore(afterOptionNode), `\nemits: {'${nameWithLoc.name}': null},`);
const lastPropertyNode = object.properties.at(-1);
if (lastPropertyNode) {
const before = propertyNodes.at(-1) || lastPropertyNode;
return fixer.insertTextAfter(before, `,\nemits: {'${nameWithLoc.name}': null}`);
} else {
const objectLeftBrace = sourceCode.getFirstToken(object, isOpeningBraceToken);
const objectRightBrace = sourceCode.getLastToken(object, isClosingBraceToken);
return fixer.insertTextAfter(objectLeftBrace, `\nemits: {'${nameWithLoc.name}': null}${objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? "" : "\n"}`);
}
}
}];
}
}));
//#endregion
Object.defineProperty(exports, 'default', {
enumerable: true,
get: function () {
return require_require_explicit_emits();
}
});