242 lines
8.3 KiB
JavaScript
242 lines
8.3 KiB
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();
|
|
}
|
|
}); |