436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
const require_runtime = require('../_virtual/_rolldown/runtime.js');
|
|
const require_index = require('../utils/index.js');
|
|
|
|
//#region lib/rules/no-use-computed-property-like-method.js
|
|
/**
|
|
* @author tyankatsu <https://github.com/tyankatsu0105>
|
|
* See LICENSE file in root directory for full license.
|
|
*/
|
|
var require_no_use_computed_property_like_method = /* @__PURE__ */ require_runtime.__commonJSMin(((exports, module) => {
|
|
const eslintUtils = require("@eslint-community/eslint-utils");
|
|
const utils = require_index.default;
|
|
/**
|
|
* @typedef {import('eslint').Scope.Scope} Scope
|
|
* @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData
|
|
* @typedef {import('../utils').GroupName} GroupName
|
|
*/
|
|
/**
|
|
* @typedef {object} CallMember
|
|
* @property {string} name
|
|
* @property {CallExpression} node
|
|
*/
|
|
/** @type {Set<GroupName>} */
|
|
const GROUPS = new Set([
|
|
"data",
|
|
"props",
|
|
"computed",
|
|
"methods"
|
|
]);
|
|
const NATIVE_NOT_FUNCTION_TYPES = new Set([
|
|
"String",
|
|
"Number",
|
|
"BigInt",
|
|
"Boolean",
|
|
"Object",
|
|
"Array",
|
|
"Symbol"
|
|
]);
|
|
/**
|
|
* @param {RuleContext} context
|
|
* @param {Expression} node
|
|
* @returns {Set<Expression>}
|
|
*/
|
|
function resolvedExpressions(context, node) {
|
|
/** @type {Map<Expression, Set<Expression>>} */
|
|
const resolvedMap = /* @__PURE__ */ new Map();
|
|
return resolvedExpressionsInternal(node);
|
|
/**
|
|
* @param {Expression} node
|
|
* @returns {Set<Expression>}
|
|
*/
|
|
function resolvedExpressionsInternal(node) {
|
|
let resolvedSet = resolvedMap.get(node);
|
|
if (!resolvedSet) {
|
|
resolvedSet = /* @__PURE__ */ new Set();
|
|
resolvedMap.set(node, resolvedSet);
|
|
for (const e of extractResolvedExpressions(node)) resolvedSet.add(e);
|
|
}
|
|
if (resolvedSet.size === 0) resolvedSet.add(node);
|
|
return resolvedSet;
|
|
}
|
|
/**
|
|
* @param {Expression} node
|
|
* @returns {Iterable<Expression>}
|
|
*/
|
|
function* extractResolvedExpressions(node) {
|
|
switch (node.type) {
|
|
case "Identifier": {
|
|
const variable = utils.findVariableByIdentifier(context, node);
|
|
if (variable) for (const ref of variable.references) {
|
|
const id = ref.identifier;
|
|
if (id.parent.type === "VariableDeclarator") {
|
|
if (id.parent.id === id && id.parent.init) yield* resolvedExpressionsInternal(id.parent.init);
|
|
} else if (id.parent.type === "AssignmentExpression" && id.parent.left === id) yield* resolvedExpressionsInternal(id.parent.right);
|
|
}
|
|
break;
|
|
}
|
|
case "ConditionalExpression":
|
|
yield* resolvedExpressionsInternal(node.consequent);
|
|
yield* resolvedExpressionsInternal(node.alternate);
|
|
break;
|
|
case "LogicalExpression":
|
|
yield* resolvedExpressionsInternal(node.left);
|
|
yield* resolvedExpressionsInternal(node.right);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Get type of props item.
|
|
* Can't consider array props like: props: {propsA: [String, Number, Function]}
|
|
* @param {RuleContext} context
|
|
* @param {ComponentObjectPropertyData} prop
|
|
* @return {string[] | null}
|
|
*
|
|
* @example
|
|
* props: {
|
|
* propA: String, // => String
|
|
* propB: {
|
|
* type: Number // => Number
|
|
* },
|
|
* }
|
|
*/
|
|
function getComponentPropsTypes(context, prop) {
|
|
const result = [];
|
|
for (const expr of resolvedExpressions(context, prop.property.value)) {
|
|
const types = getComponentPropsTypesFromExpression(expr);
|
|
if (types == null) return null;
|
|
result.push(...types);
|
|
}
|
|
return result;
|
|
/**
|
|
* @param {Expression} expr
|
|
*/
|
|
function getComponentPropsTypesFromExpression(expr) {
|
|
let typeExprs;
|
|
/**
|
|
* Check object props `props: { objectProps: {...} }`
|
|
*/
|
|
if (expr.type === "ObjectExpression") {
|
|
const type = utils.findProperty(expr, "type");
|
|
if (type == null) return null;
|
|
typeExprs = resolvedExpressions(context, type.value);
|
|
} else typeExprs = [expr];
|
|
const result = [];
|
|
for (const typeExpr of typeExprs) {
|
|
const types = getComponentPropsTypesFromTypeExpression(typeExpr);
|
|
if (types == null) return null;
|
|
result.push(...types);
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* @param {Expression} typeExpr
|
|
*/
|
|
function getComponentPropsTypesFromTypeExpression(typeExpr) {
|
|
if (typeExpr.type === "Identifier") return [typeExpr.name];
|
|
if (typeExpr.type === "ArrayExpression") {
|
|
const types = [];
|
|
for (const element of typeExpr.elements) {
|
|
if (!element) continue;
|
|
if (element.type === "SpreadElement") return null;
|
|
for (const elementExpr of resolvedExpressions(context, element)) {
|
|
if (elementExpr.type !== "Identifier") return null;
|
|
types.push(elementExpr.name);
|
|
}
|
|
}
|
|
return types;
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Check whether given expression may be a function.
|
|
* @param {RuleContext} context
|
|
* @param {Expression} node
|
|
* @returns {boolean}
|
|
*/
|
|
function maybeFunction(context, node) {
|
|
for (const expr of resolvedExpressions(context, node)) {
|
|
if (expr.type === "ObjectExpression" || expr.type === "ArrayExpression" || expr.type === "Literal" || expr.type === "TemplateLiteral" || expr.type === "BinaryExpression" || expr.type === "UnaryExpression" || expr.type === "UpdateExpression") continue;
|
|
if (expr.type === "ConditionalExpression" && !maybeFunction(context, expr.consequent) && !maybeFunction(context, expr.alternate)) continue;
|
|
const evaluated = eslintUtils.getStaticValue(expr, utils.getScope(context, expr));
|
|
if (!evaluated) return true;
|
|
if (typeof evaluated.value === "function") return true;
|
|
}
|
|
return false;
|
|
}
|
|
var FunctionData = class {
|
|
/**
|
|
* @param {string} name
|
|
* @param {'methods' | 'computed'} kind
|
|
* @param {FunctionExpression | ArrowFunctionExpression} node
|
|
* @param {RuleContext} context
|
|
*/
|
|
constructor(name, kind, node, context) {
|
|
this.context = context;
|
|
this.name = name;
|
|
this.kind = kind;
|
|
this.node = node;
|
|
/** @type {(Expression | null)[]} */
|
|
this.returnValues = [];
|
|
/** @type {boolean | null} */
|
|
this.cacheMaybeReturnFunction = null;
|
|
}
|
|
/**
|
|
* @param {Expression | null} node
|
|
*/
|
|
addReturnValue(node) {
|
|
this.returnValues.push(node);
|
|
}
|
|
/**
|
|
* @param {ComponentStack} component
|
|
*/
|
|
maybeReturnFunction(component) {
|
|
if (this.cacheMaybeReturnFunction != null) return this.cacheMaybeReturnFunction;
|
|
this.cacheMaybeReturnFunction = true;
|
|
return this.cacheMaybeReturnFunction = this.returnValues.some((returnValue) => returnValue && component.maybeFunctionExpression(returnValue));
|
|
}
|
|
};
|
|
/** Component information class. */
|
|
var ComponentStack = class {
|
|
/**
|
|
* @param {ObjectExpression} node
|
|
* @param {RuleContext} context
|
|
* @param {ComponentStack | null} upper
|
|
*/
|
|
constructor(node, context, upper) {
|
|
this.node = node;
|
|
this.context = context;
|
|
/** Upper scope component */
|
|
this.upper = upper;
|
|
/** @type {Map<string, boolean>} */
|
|
const maybeFunctions = /* @__PURE__ */ new Map();
|
|
/** @type {FunctionData[]} */
|
|
const functions = [];
|
|
for (const property of utils.iterateProperties(node, GROUPS)) {
|
|
if (property.type === "array") continue;
|
|
switch (property.groupName) {
|
|
case "data":
|
|
maybeFunctions.set(property.name, maybeFunction(context, property.property.value));
|
|
break;
|
|
case "props": {
|
|
const types = getComponentPropsTypes(context, property);
|
|
maybeFunctions.set(property.name, !types || types.some((type) => !NATIVE_NOT_FUNCTION_TYPES.has(type)));
|
|
break;
|
|
}
|
|
case "computed": {
|
|
let value = property.property.value;
|
|
if (value.type === "ObjectExpression") {
|
|
const getProp = utils.findProperty(value, "get");
|
|
if (getProp) value = getProp.value;
|
|
}
|
|
processFunction(property.name, value, "computed");
|
|
break;
|
|
}
|
|
case "methods": {
|
|
const value = property.property.value;
|
|
processFunction(property.name, value, "methods");
|
|
maybeFunctions.set(property.name, true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.maybeFunctions = maybeFunctions;
|
|
this.functions = functions;
|
|
/** @type {CallMember[]} */
|
|
this.callMembers = [];
|
|
/** @type {Map<Expression, boolean>} */
|
|
this.cacheMaybeFunctionExpressions = /* @__PURE__ */ new Map();
|
|
/**
|
|
* @param {string} name
|
|
* @param {Expression} value
|
|
* @param {'methods' | 'computed'} kind
|
|
*/
|
|
function processFunction(name, value, kind) {
|
|
if (value.type === "FunctionExpression") functions.push(new FunctionData(name, kind, value, context));
|
|
else if (value.type === "ArrowFunctionExpression") {
|
|
const data = new FunctionData(name, kind, value, context);
|
|
if (value.expression) data.addReturnValue(value.body);
|
|
functions.push(data);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Adds the given return statement to the return value of the function.
|
|
* @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} scopeFunction
|
|
* @param {ReturnStatement} returnNode
|
|
*/
|
|
addReturnStatement(scopeFunction, returnNode) {
|
|
for (const data of this.functions) if (data.node === scopeFunction) {
|
|
data.addReturnValue(returnNode.argument);
|
|
break;
|
|
}
|
|
}
|
|
verifyComponent() {
|
|
for (const call of this.callMembers) this.verifyCallMember(call);
|
|
}
|
|
/**
|
|
* @param {CallMember} call
|
|
*/
|
|
verifyCallMember(call) {
|
|
const fnData = this.functions.find((data) => data.name === call.name && data.kind === "computed");
|
|
if (!fnData) return;
|
|
if (!fnData.maybeReturnFunction(this)) {
|
|
const prefix = call.node.callee.type === "MemberExpression" ? "this." : "";
|
|
this.context.report({
|
|
node: call.node,
|
|
messageId: "unexpected",
|
|
data: {
|
|
likeProperty: `${prefix}${call.name}`,
|
|
likeMethod: `${prefix}${call.name}()`
|
|
}
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Check whether given expression may be a function.
|
|
* @param {Expression} node
|
|
* @returns {boolean}
|
|
*/
|
|
maybeFunctionExpression(node) {
|
|
const cache = this.cacheMaybeFunctionExpressions.get(node);
|
|
if (cache != null) return cache;
|
|
this.cacheMaybeFunctionExpressions.set(node, true);
|
|
const result = maybeFunctionExpressionWithoutCache.call(this);
|
|
this.cacheMaybeFunctionExpressions.set(node, result);
|
|
return result;
|
|
/**
|
|
* @this {ComponentStack}
|
|
*/
|
|
function maybeFunctionExpressionWithoutCache() {
|
|
for (const expr of resolvedExpressions(this.context, node)) {
|
|
if (!maybeFunction(this.context, expr)) continue;
|
|
switch (expr.type) {
|
|
case "MemberExpression":
|
|
if (utils.isThis(expr.object, this.context)) {
|
|
const name = utils.getStaticPropertyName(expr);
|
|
if (name && !this.maybeFunctionProperty(name)) continue;
|
|
}
|
|
break;
|
|
case "CallExpression":
|
|
if (expr.callee.type === "MemberExpression" && utils.isThis(expr.callee.object, this.context)) {
|
|
const name = utils.getStaticPropertyName(expr.callee);
|
|
const fnData = this.functions.find((data) => data.name === name);
|
|
if (fnData && fnData.kind === "methods" && !fnData.maybeReturnFunction(this)) continue;
|
|
}
|
|
break;
|
|
case "ConditionalExpression":
|
|
if (!this.maybeFunctionExpression(expr.consequent) && !this.maybeFunctionExpression(expr.alternate)) continue;
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Check whether given property name may be a function.
|
|
* @param {string} name
|
|
* @returns {boolean}
|
|
*/
|
|
maybeFunctionProperty(name) {
|
|
const cache = this.maybeFunctions.get(name);
|
|
if (cache != null) return cache;
|
|
this.maybeFunctions.set(name, true);
|
|
const result = maybeFunctionPropertyWithoutCache.call(this);
|
|
this.maybeFunctions.set(name, result);
|
|
return result;
|
|
/**
|
|
* @this {ComponentStack}
|
|
*/
|
|
function maybeFunctionPropertyWithoutCache() {
|
|
const fnData = this.functions.find((data) => data.name === name);
|
|
if (fnData && fnData.kind === "computed") return fnData.maybeReturnFunction(this);
|
|
return true;
|
|
}
|
|
}
|
|
};
|
|
module.exports = {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "disallow use computed property like method",
|
|
categories: ["vue3-essential", "vue2-essential"],
|
|
url: "https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html"
|
|
},
|
|
fixable: null,
|
|
schema: [],
|
|
messages: { unexpected: "Use {{ likeProperty }} instead of {{ likeMethod }}." }
|
|
},
|
|
create(context) {
|
|
/**
|
|
* @typedef {object} ScopeStack
|
|
* @property {ScopeStack | null} upper
|
|
* @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
|
|
*/
|
|
/** @type {ScopeStack | null} */
|
|
let scopeStack = null;
|
|
/** @type {ComponentStack | null} */
|
|
let componentStack = null;
|
|
/** @type {ComponentStack | null} */
|
|
let templateComponent = null;
|
|
return utils.compositingVisitors({}, utils.defineVueVisitor(context, {
|
|
onVueObjectEnter(node) {
|
|
componentStack = new ComponentStack(node, context, componentStack);
|
|
if (!templateComponent && utils.isInExportDefault(node)) templateComponent = componentStack;
|
|
},
|
|
onVueObjectExit() {
|
|
if (componentStack) {
|
|
componentStack.verifyComponent();
|
|
componentStack = componentStack.upper;
|
|
}
|
|
},
|
|
":function"(node) {
|
|
scopeStack = {
|
|
upper: scopeStack,
|
|
scopeNode: node
|
|
};
|
|
},
|
|
ReturnStatement(node) {
|
|
if (scopeStack && componentStack) componentStack.addReturnStatement(scopeStack.scopeNode, node);
|
|
},
|
|
":function:exit"() {
|
|
scopeStack = scopeStack && scopeStack.upper;
|
|
},
|
|
"ThisExpression, Identifier"(node) {
|
|
if (!componentStack || node.parent.type !== "MemberExpression" || node.parent.object !== node || node.parent.parent.type !== "CallExpression" || node.parent.parent.callee !== node.parent || !utils.isThis(node, context)) return;
|
|
const name = utils.getStaticPropertyName(node.parent);
|
|
if (name) componentStack.callMembers.push({
|
|
name,
|
|
node: node.parent.parent
|
|
});
|
|
}
|
|
}), utils.defineTemplateBodyVisitor(context, { VExpressionContainer(node) {
|
|
if (!templateComponent) return;
|
|
for (const id of node.references.filter((ref) => ref.variable == null).map((ref) => ref.id)) {
|
|
if (id.parent.type !== "CallExpression" || id.parent.callee !== id) continue;
|
|
templateComponent.verifyCallMember({
|
|
name: id.name,
|
|
node: id.parent
|
|
});
|
|
}
|
|
} }));
|
|
}
|
|
};
|
|
}));
|
|
|
|
//#endregion
|
|
Object.defineProperty(exports, 'default', {
|
|
enumerable: true,
|
|
get: function () {
|
|
return require_no_use_computed_property_like_method();
|
|
}
|
|
}); |