123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- /**
- * @author tyankatsu <https://github.com/tyankatsu0105>
- * See LICENSE file in root directory for full license.
- */
- 'use strict'
- const eslintUtils = require('@eslint-community/eslint-utils')
- const utils = require('../utils')
- /**
- * @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 = new Map()
- return resolvedExpressionsInternal(node)
- /**
- * @param {Expression} node
- * @returns {Set<Expression>}
- */
- function resolvedExpressionsInternal(node) {
- let resolvedSet = resolvedMap.get(node)
- if (!resolvedSet) {
- resolvedSet = 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) {
- // It could be a function because we don't know what it is.
- return true
- }
- if (typeof evaluated.value === 'function') {
- return true
- }
- }
- return false
- }
- class FunctionData {
- /**
- * @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
- }
- // Avoid infinite recursion.
- this.cacheMaybeReturnFunction = true
- return (this.cacheMaybeReturnFunction = this.returnValues.some(
- (returnValue) =>
- returnValue && component.maybeFunctionExpression(returnValue)
- ))
- }
- }
- /** Component information class. */
- class ComponentStack {
- /**
- * @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 = new Map()
- /** @type {FunctionData[]} */
- const functions = []
- // Extract properties
- 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 = 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) {
- // It is not computed, or unknown.
- 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
- }
- // Avoid infinite recursion.
- 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
- }
- }
- // It could be a function because we don't know what it is.
- 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
- }
- // Avoid infinite recursion.
- 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)
- }
- // It could be a function because we don't know what it is.
- 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 }}.'
- }
- },
- /** @param {RuleContext} context */
- 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
- }
- },
- /**
- * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
- */
- ':function'(node) {
- scopeStack = {
- upper: scopeStack,
- scopeNode: node
- }
- },
- ReturnStatement(node) {
- if (scopeStack && componentStack) {
- componentStack.addReturnStatement(scopeStack.scopeNode, node)
- }
- },
- ':function:exit'() {
- scopeStack = scopeStack && scopeStack.upper
- },
- /**
- * @param {ThisExpression | Identifier} node
- */
- '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, {
- /**
- * @param {VExpressionContainer} node
- */
- 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
- })
- }
- }
- })
- )
- }
- }
|