eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
242 lines (239 loc) • 8.5 kB
JavaScript
'use strict';
const require_runtime = require('../_virtual/_rolldown/runtime.js');
const require_index = require('../utils/index.js');
//#region lib/rules/require-expose.js
/**
* @fileoverview Require `expose` in Vue components
* @author Yosuke Ota <https://github.com/ota-meshi>
*/
var require_require_expose = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
const { findVariable, isOpeningBraceToken, isClosingBraceToken } = require("@eslint-community/eslint-utils");
const utils = require_index.default;
const { getVueComponentDefinitionType } = require_index.default;
const FIX_EXPOSE_BEFORE_OPTIONS = new Set([
"name",
"components",
"directives",
"extends",
"mixins",
"provide",
"inject",
"inheritAttrs",
"props",
"emits"
]);
/**
* @param {Property | SpreadElement} node
* @returns {node is ObjectExpressionProperty}
*/
function isExposeProperty(node) {
return node.type === "Property" && utils.getStaticPropertyName(node) === "expose" && !node.computed;
}
/**
* Get the callee member node from the given CallExpression
* @param {CallExpression} node CallExpression
*/
function getCalleeMemberNode(node) {
const callee = utils.skipChainExpression(node.callee);
if (callee.type === "MemberExpression") {
const name = utils.getStaticPropertyName(callee);
if (name) return {
name,
member: callee
};
}
return null;
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "require declare public properties using `expose`",
categories: void 0,
url: "https://eslint.vuejs.org/rules/require-expose.html"
},
fixable: null,
hasSuggestions: true,
schema: [],
messages: {
requireExpose: "The public properties of the component must be explicitly declared using `expose`. If the component does not have public properties, declare it empty.",
addExposeOptionForEmpty: "Add the `expose` option to give an empty array.",
addExposeOptionForAll: "Add the `expose` option to declare all properties."
}
},
create(context) {
if (utils.isScriptSetup(context)) return {};
/**
* @typedef {object} SetupContext
* @property {Set<Identifier>} exposeReferenceIds
* @property {Set<Identifier>} contextReferenceIds
*/
/** @type {Map<ObjectExpression, SetupContext>} */
const setupContexts = /* @__PURE__ */ new Map();
/** @type {Set<ObjectExpression>} */
const calledExpose = /* @__PURE__ */ new Set();
/**
* @typedef {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} FunctionNode
*/
/**
* @typedef {object} ScopeStack
* @property {ScopeStack | null} upper
* @property {FunctionNode} functionNode
* @property {boolean} returnFunction
*/
/**
* @type {ScopeStack | null}
*/
let scopeStack = null;
/** @type {Map<FunctionNode, ObjectExpression>} */
const setupFunctions = /* @__PURE__ */ new Map();
/** @type {Set<ObjectExpression>} */
const setupRender = /* @__PURE__ */ new Set();
/**
* @param {Expression} node
* @returns {boolean}
*/
function isFunction(node) {
if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") return true;
if (node.type === "Identifier") {
const variable = findVariable(utils.getScope(context, node), node);
if (variable) for (const def of variable.defs) {
if (def.type === "FunctionName") return true;
if (def.type === "Variable" && def.node.init) return isFunction(def.node.init);
}
}
return false;
}
return utils.defineVueVisitor(context, {
onSetupFunctionEnter(node, { node: vueNode }) {
setupFunctions.set(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 exposeReferenceIds = /* @__PURE__ */ new Set();
if (contextParam.type === "ObjectPattern") {
const exposeProperty = utils.findAssignmentProperty(contextParam, "expose");
if (!exposeProperty) return;
const exposeParam = exposeProperty.value;
const variable = exposeParam.type === "Identifier" ? findVariable(utils.getScope(context, exposeParam), exposeParam) : null;
if (!variable) return;
for (const reference of variable.references) {
if (!reference.isRead()) continue;
exposeReferenceIds.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,
exposeReferenceIds
});
},
CallExpression(node, { node: vueNode }) {
if (calledExpose.has(vueNode)) return;
const setupContext = setupContexts.get(vueNode);
if (setupContext) {
const { contextReferenceIds, exposeReferenceIds } = setupContext;
if (node.callee.type === "Identifier" && exposeReferenceIds.has(node.callee)) calledExpose.add(vueNode);
else {
const expose = getCalleeMemberNode(node);
if (expose && expose.name === "expose" && expose.member.object.type === "Identifier" && contextReferenceIds.has(expose.member.object)) calledExpose.add(vueNode);
}
}
},
":function"(node) {
scopeStack = {
upper: scopeStack,
functionNode: node,
returnFunction: false
};
if (node.type === "ArrowFunctionExpression" && node.expression && isFunction(node.body)) scopeStack.returnFunction = true;
},
ReturnStatement(node) {
if (!scopeStack) return;
if (!scopeStack.returnFunction && node.argument && isFunction(node.argument)) scopeStack.returnFunction = true;
},
":function:exit"(node) {
if (scopeStack && scopeStack.returnFunction) {
const vueNode = setupFunctions.get(node);
if (vueNode) setupRender.add(vueNode);
}
scopeStack = scopeStack && scopeStack.upper;
},
onVueObjectExit(component, { type }) {
if (calledExpose.has(component)) return;
if (setupRender.has(component)) return;
if (type === "definition") {
if (getVueComponentDefinitionType(component) === "mixin") return;
}
if (component.properties.some(isExposeProperty)) return;
context.report({
node: component,
messageId: "requireExpose",
suggest: buildSuggest(component, context)
});
}
});
}
};
/**
* @param {ObjectExpression} object
* @param {RuleContext} context
* @returns {Rule.SuggestionReportDescriptor[]}
*/
function buildSuggest(object, context) {
const propertyNodes = object.properties.filter(utils.isProperty);
const sourceCode = context.sourceCode;
const beforeOptionNode = propertyNodes.find((p) => FIX_EXPOSE_BEFORE_OPTIONS.has(utils.getStaticPropertyName(p) || ""));
const allProps = [...new Set(utils.iterateProperties(object, new Set([
"props",
"data",
"computed",
"setup",
"methods",
"watch"
])))];
return [{
messageId: "addExposeOptionForEmpty",
fix: buildFix("expose: []")
}, ...allProps.length > 0 ? [{
messageId: "addExposeOptionForAll",
fix: buildFix(`expose: [${allProps.map((p) => JSON.stringify(p.name)).join(", ")}]`)
}] : []];
/**
* @param {string} text
*/
function buildFix(text) {
/**
* @param {RuleFixer} fixer
*/
return (fixer) => {
if (beforeOptionNode) return fixer.insertTextAfter(beforeOptionNode, `,\n${text}`);
else if (object.properties.length > 0) {
const after = propertyNodes[0] || object.properties[0];
return fixer.insertTextAfter(sourceCode.getTokenBefore(after), `\n${text},`);
} else {
const objectLeftBrace = sourceCode.getFirstToken(object, isOpeningBraceToken);
const objectRightBrace = sourceCode.getLastToken(object, isClosingBraceToken);
return fixer.insertTextAfter(objectLeftBrace, `\n${text}${objectLeftBrace.loc.end.line < objectRightBrace.loc.start.line ? "" : "\n"}`);
}
};
}
}
}));
//#endregion
Object.defineProperty(exports, 'default', {
enumerable: true,
get: function () {
return require_require_expose();
}
});